> On Sep 8, 2017, at 11:32 AM, Tony Allevato <tony.allev...@gmail.com> wrote:
> 
> 
> 
> On Fri, Sep 8, 2017 at 8:35 AM Matthew Johnson <matt...@anandabits.com 
> <mailto:matt...@anandabits.com>> wrote:
>> On Sep 8, 2017, at 9:53 AM, Tony Allevato via swift-evolution 
>> <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>> 
>> Thanks for bringing this up, Logan! It's something I've been thinking about 
>> a lot lately after a conversation with some colleagues outside of this 
>> community. Some of my thoughts:
>> 
>> AFAIK, there are two major use cases here: (1) you need the whole collection 
>> of cases, like in your example, and (2) you just need the number of cases. 
>> The latter seems to occur somewhat commonly when people want to use an enum 
>> to define the sections of, say, a UITableView. They just return the count 
>> from numberOfSections(in:) and then switch over the cases in their 
>> cell-providing methods.
>> 
>> Because of #2, it would be nice to avoid instantiating the collection 
>> eagerly. (Also because of examples like Jonathan's, where the enum is 
>> large.) If all the user is ever really doing is iterating over them, there's 
>> no need to keep the entire collection in memory. This leads us to look at 
>> Sequence; we could use something like AnySequence to keep the current case 
>> as our state and a transition function to advance to the next one. If a user 
>> needs to instantiate the full array from that sequence they can do so, but 
>> they have to do it explicitly.
>> 
>> The catch is that Sequence only provides `underestimatedCount`, rather than 
>> `count`. Calling the former would be an awkward API (why is it 
>> underestimated? we know how many cases there are). I suppose we could create 
>> a concrete wrapper for Sequence (PrecountedSequence?) that provides a 
>> `count` property to make that cleaner, and then have `underestimatedCount` 
>> return the same thing if users passed this thing into a generic operation 
>> constrained over Sequence. (The standard library already has support 
>> wrappers like EnumeratedSequence, so maybe this is appropriate.)
>> 
>> Another question that would need to be answered is, how should the cases be 
>> ordered? Declaration order seems obvious and straightforward, but if you 
>> have a raw-value enum (say, integers), you could have the declaration order 
>> and the numeric order differ. Maybe that's not a problem. Tying the 
>> iteration order to declaration order also means that the behavior of a 
>> program could change simply by reördering the cases. Maybe that's not a big 
>> problem either, but it's something to call out.
>> 
>> If I were designing this, I'd start with the following approach. First, add 
>> a new protocol to the standard library:
>> 
>> ```
>> public protocol ValueEnumerable {
>>   associatedtype AllValuesSequence: Sequence where 
>> AllValuesSequence.Iterator.Element == Self
>> 
>>   static var allValues: AllValuesSequence { get }
>> }
>> ```
>> 
>> Then, for enums that declare conformance to that protocol, synthesize the 
>> body of `allValues` to return an appropriate sequence. If we imagine a model 
>> like AnySequence, then the "state" can be the current case, and the 
>> transition function can be a switch/case that returns it and advances to the 
>> next one (finally returning nil).
>> 
>> There's an opportunity for optimization that may or may not be worth it: if 
>> the enum is RawRepresentable with RawValue == Int, AND all the raw values 
>> are in a contiguous range, AND declaration order is numerical order 
>> (assuming we kept that constraint), then the synthesized state machine can 
>> just be a simple integer incrementation and call to `init?(rawValue:)`. When 
>> all the cases have been generated, that will return nil on its own.
>> 
>> So that covers enums without associated values. What about those with 
>> associated values? I would argue that the "number of cases" isn't something 
>> that's very useful here—if we consider that enum cases are really factory 
>> functions for concrete values of the type, then we shouldn't think about 
>> "what are all the cases of this enum" but "what are all the values of this 
>> type". (For enums without associated values, those are synonymous.)
>> 
>> An enum with associated values can potentially have an infinite number of 
>> values. Here's one:
>> 
>> ```
>> enum BinaryTree {
>>   case subtree(left: BinaryTree, right: BinaryTree)
>>   case leaf
>>   case empty
>> }
>> ```
>> 
>> Even without introducing an Element type in the leaf nodes, there are a 
>> countably infinite number of binary trees. So first off, we wouldn't be able 
>> to generate a meaningful `count` property for that. Since they're countably 
>> infinite, we *could* theoretically lazily generate a sequence of them! It 
>> would be a true statement to say "an enum with associated values can have 
>> all of its values enumerated if all of its associated values are also 
>> ValueEnumerable". But I don't think that's something we could have the 
>> compiler synthesize generally: the logic to tie the sequences together would 
>> be quite complex in the absence of a construct like coroutines/yield, and 
>> what's worse, the compiler would have to do some deeper analysis to avoid 
>> infinite recursion. For example, if it used the naïve approach of generating 
>> the elements in declaration order, it would keep drilling down into the 
>> `subtree` case above over and over; it really needs to hit the base cases 
>> first, and requiring the user to order the cases in a certain way for it to 
>> just work at all is a non-starter.
>> 
>> So, enums with associated values are probably left unsynthesized. But the 
>> interesting thing about having this be a standard protocol is that there 
>> would be nothing stopping a user from conforming to it and implementing it 
>> manually, not only for enums but for other types as well. The potential may 
>> exist for some interesting algorithms by doing that, but I haven't thought 
>> that far ahead.
>> 
>> There are probably some things I'm missing here, but I'd love to hear other 
>> people's thoughts on it.
> 
> There are some things I really like about this approach, but it doesn’t quite 
> align with a lot of the usage I have seen for manually declared `allValues` 
> pattern.  
> 
> One of the most common ways I have seen `allValues` used is as a 
> representation of static sections or rows backing table or collection views.  
> Code written like this will take the section or item index provided by a data 
> source or delegate method and index into an `allValues` array to access the 
> corresponding value.  These methods usually access one or more members of the 
> value or pass it along to something else (often a cell) which does so.  
> 
> If we introduce synthesis that doesn’t support this use case I think a lot 
> people will be frustrated so my opinion is that we need to support it.  This 
> means users need a way to request synthesis of a `Collection` with an `Int` 
> index.  Obviously doing this solves the `count` problem.  The collection 
> would not need to be eager.  It could be implemented to produce values on 
> demand rather than storing them.  
> 
> Great points! I was only considering the table view/section case where the 
> enum had raw values 0..<count, but I do imagine it's possible that someone 
> could just define `enum Section { case header, content, footer }` and then 
> want to turn an IndexPath value into the appropriate Section.
> 
> On the other hand, though, isn't that what raw value enums are for? If the 
> user needs to do what you're saying—map specific integers to enum 
> values—shouldn't they do so by giving those cases raw values and calling 
> init?(rawValue:), not by indexing into a collection? Especially since they 
> can already do that today, and the only thing they're missing is being able 
> to retrieve the count, which a "PrecountedSequence" mentioned above, or 
> something like it, could also provide.

First, I’m making observations about what people are doing, not what they could 
do.  

Second, the raw value may not correspond to 0-based indices.  It might not even 
be an Int.  There is no reason to couple this common use case of `allValues` to 
`Int` raw values with 0-based indices.

Third, `init(rawValue:)` is a failable initializer and would require a force 
unwrap.  If the raw values *are* 0-based integers this is similar to the 
collection bounds check that would be necessary, but it moves it into user 
code.  People don’t like writing force unwraps.

> 
> My main concern with providing a Collection with Int indices is that, at some 
> fundamental/theoretical level, it feels like it only makes sense for enums 
> with contiguous numeric raw values. For other kinds of enums, including those 
> where the enum is just a "bag of things" without raw values, it feels 
> artificial.

Sure, that’s why I proposed a couple of options for addressing both use cases.  
I think both have merit.  I also think we need to recognize that most people 
are asking for a replacement for manually writing a static array and won’t be 
satisfied unless we provide a solution where the synthesized property behaves 
similarly.

> 
>  
> Of course there might be some cases where a manual implementation is 
> necessary but implementing `Collection` is not desirable for one reason or 
> another.  One way to solve both of these use cases would be to have a 
> protocol hierarchy but that seems like it might be excessively complex for a 
> feature like this.  Another way might be to take advantage of the fact that 
> in the use case mentioned above people are usually working with the concrete 
> type.  We could allow the compiler to synthesize an implementation that 
> *exceeds* the requirement of the protocol such that the synthesized 
> `AllValuesSequence` is actually a `Collection where Index == Int`.  I’m not 
> sure which option is better.
> 
> I would also like to discuss enums with associated values.  It would 
> certainly be reasonable to disallow synthesis for these types in an initial 
> implementation.  I don’t know of any use cases off the top of my head 
> (although I expect some good ones do exist).  That said, I don’t think 
> synthesis would be prohibitive for enums with associated values so long as 
> the type of all associated values conforms to `ValueEnumerable`.  We should 
> probably support synthesis for these types eventually, possibly in the 
> initial implementation if there are no significant implementation barriers.
> 
> I mentioned some of those barriers above. One issue is that synthesizing the 
> code to lazily (i.e., reëntrantly) generate a sequence whose elements are the 
> Cartesian products of other sequences is non-trivial. (Coroutines/yield would 
> make this a piece of cake.)

The good news is that we might be in luck on this front in the Swift 5 
timeframe.  :)

> 
> The other is the issue with recursive enums, like the BinaryTree example, 
> where the compiler has to know to synthesize them in a particular order or 
> else it will recurse indefinitely before even producing its first value. 
> However, this could be addressed by simply forbidding automatic synthesis of 
> enums that have an indirect case, which is probably a reasonable limitation.

Yeah, that seems like a reasonable limitation.

> 
>  
> 
> That’s my two cents.
> 
> - Matthew
> 
>> 
>> 
>> On Fri, Sep 8, 2017 at 3:40 AM Jonathan Hull via swift-evolution 
>> <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>> +1000
>> 
>> I once made a country code enum, and creating that array was simple, but 
>> took forever, and was prone to mistakes.
>> 
>> Thanks,
>> Jon
>> 
>> > On Sep 8, 2017, at 2:56 AM, Logan Shire via swift-evolution 
>> > <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>> >
>> > Googling ‘swift iterate over enum cases’ yields many results of various 
>> > levels of hackery.
>> > Obviously it’s trivial to write a computed property that returns an enum’s 
>> > cases as an
>> > array, but maintaining that is prone to error. If you add another case, 
>> > you need to make sure
>> > you update the array property. For enums without associated types,
>> > I propose adding a synthesized static var, ‘cases', to the enum’s type. 
>> > E.g.
>> >
>> > enum Suit: String {
>> >    case spades = "♠"
>> >    case hearts = "♥"
>> >    case diamonds = "♦"
>> >    case clubs = "♣"
>> > }
>> >
>> > let values = (1…13).map { value in
>> >    switch value {
>> >    case 1: return “A”
>> >    case 11: return “J”
>> >    case 12: return “Q”
>> >    case 13: return “K”
>> >    default: return String(value)
>> >    }
>> > }
>> >
>> > let cards = values.flatMap { value in Suit.cases.map { “\($0)\(value)"  } }
>> >
>> > Yields [“♠A”, “ ♥ A”, …, “♣K”]
>> > Thoughts?
>> >
>> >
>> > Thanks!
>> > - Logan Shire
>> > _______________________________________________
>> > swift-evolution mailing list
>> > swift-evolution@swift.org <mailto:swift-evolution@swift.org>
>> > https://lists.swift.org/mailman/listinfo/swift-evolution 
>> > <https://lists.swift.org/mailman/listinfo/swift-evolution>
>> 
>> _______________________________________________
>> swift-evolution mailing list
>> swift-evolution@swift.org <mailto:swift-evolution@swift.org>
>> https://lists.swift.org/mailman/listinfo/swift-evolution 
>> <https://lists.swift.org/mailman/listinfo/swift-evolution>
>> _______________________________________________
>> swift-evolution mailing list
>> swift-evolution@swift.org <mailto:swift-evolution@swift.org>
>> https://lists.swift.org/mailman/listinfo/swift-evolution 
>> <https://lists.swift.org/mailman/listinfo/swift-evolution>
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

Reply via email to