> On Sep 8, 2017, at 12:05 PM, Tony Allevato <[email protected]> wrote: > > > > On Fri, Sep 8, 2017 at 9:44 AM Matthew Johnson <[email protected] > <mailto:[email protected]>> wrote: >> On Sep 8, 2017, at 11:32 AM, Tony Allevato <[email protected] >> <mailto:[email protected]>> wrote: >> >> >> >> On Fri, Sep 8, 2017 at 8:35 AM Matthew Johnson <[email protected] >> <mailto:[email protected]>> wrote: >>> On Sep 8, 2017, at 9:53 AM, Tony Allevato via swift-evolution >>> <[email protected] <mailto:[email protected]>> 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. > > Do we know of any examples where a user is both (1) defining an enum with > integer raw values that are noncontiguous or non-zero-based and (2) need > declaration-ordinal-based indexing into those cases for other reasons, like a > table/collection view? I can't think of why someone would do that, but I'm > happy to consider something that I'm missing.
I don’t off-hand, but I don’t think the lack of example is a good motivation for a solution that doesn’t directly address the most commonly known use case for this feature. > > > 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. > > Yeah, this is a really good point that I wasn't fully considering. If other > invariants in the application hold—such as table view cell functions never > receiving a section index outside 0..<count—then unwrapping it just forces > users to address a situation that will never actually occur unless UIKit is > fundamentally broken. Right, but the most crucial point is that it forces *user* to address this. They are not required to today. It is handled by the bounds check in Array. This might sound like splitting hairs but I think there are a lot of people who wouldn't view it that way. > > > >> >> 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. > > Agreed—I just wanted to point out the distinction because an important part > of fleshing this out will be to partition the various "classes" of enums into > those that would receive an indexable Collection vs. those that would receive > just a Sequence. I agree that it’s an important distinction. To be honest, I’m not sure there is a good way to solve both usages without introducing more complexity than would be acceptable for something like this. It might be a problem better solved by macros or some other metaprogramming feature. It would be unfortunate to have to wait until we have those to solve this. However, I don’t think it's an important enough problem to deserve a solution with a lot of knobs and associated complexity. > > > >> >> >> 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. :) > > Fingers crossed! I'm not a concurrency expert by any means, so the most > exciting part of those new proposals to me is the side-effect that we might > get something like C# enumerators :) > > > >> >> 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 >>> <[email protected] <mailto:[email protected]>> 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 >>> > <[email protected] <mailto:[email protected]>> 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 >>> > [email protected] <mailto:[email protected]> >>> > https://lists.swift.org/mailman/listinfo/swift-evolution >>> > <https://lists.swift.org/mailman/listinfo/swift-evolution> >>> >>> _______________________________________________ >>> swift-evolution mailing list >>> [email protected] <mailto:[email protected]> >>> https://lists.swift.org/mailman/listinfo/swift-evolution >>> <https://lists.swift.org/mailman/listinfo/swift-evolution> >>> _______________________________________________ >>> swift-evolution mailing list >>> [email protected] <mailto:[email protected]> >>> https://lists.swift.org/mailman/listinfo/swift-evolution >>> <https://lists.swift.org/mailman/listinfo/swift-evolution>
_______________________________________________ swift-evolution mailing list [email protected] https://lists.swift.org/mailman/listinfo/swift-evolution
