[swift-evolution] [swift-dev] Re-pitch: Deriving collections of enum cases

Brent Royal-Gordon brent at architechies.com
Mon Nov 13 20:16:29 CST 2017

> On Nov 12, 2017, at 10:16 AM, Xiaodi Wu <xiaodi.wu at gmail.com> wrote:
> On Sun, Nov 12, 2017 at 4:54 AM, Brent Royal-Gordon <brent at architechies.com <mailto:brent at architechies.com>> wrote:
>> On Nov 10, 2017, at 11:01 PM, Xiaodi Wu <xiaodi.wu at gmail.com <mailto:xiaodi.wu at gmail.com>> wrote:
>> Nit: if you want to call it `ValueEnumerable`, then this should be `DefaultValueCollection`.
> I used `DefaultCaseCollection` because, although the protocol can work with any type to return its values, this type can only work with enums and only returns their cases. `ValueEnumerable` could be sensibly applied to `Int`; `DefaultCaseCollection` could not.
>  Because of how you've chosen to implement `DefaultCaseCollection`, or do you mean to say that you deliberately want a design where types other than enums do not share the default return type?

Because the way `DefaultCaseCollection` works is that it queries runtime metadata that can only exist for enums.

In theory, we might be able to write a `DefaultValueCollection` which would work for structs with `ValueEnumerable` properties by generating all possible permutations of those fields. In practice, I suspect that this would only rarely be useful. Structs' types are rarely specified so tightly that all permutations are valid; for instance, a `struct PlayingCard` with an integer `rank` property would only be *valid* with a rank between 1 and 13, even though `Int`'s range is much wider. So I don't think we'll ever want this type to support structs, and it would therefore be clearer to bake its enum-only nature into its name.

(If your response is that "your argument against permuting all possible struct values is just as true with an integer associated value"…well, you're not wrong, and that might be an argument against making integer types `ValueEnumerable`. But we don't propose conforming `Int` to `ValueEnumerable` immediately, just adopting a design flexible enough to permit it.)

> Tony's "no more specific than they need to" language applies here. The way I see it is this:
> * `Bool` is not an enum, but it could be usefully conformed to `ValueEnumerable`. Why should we prevent that?
> * A type with two independently-switchable `Bool`s—say, `isMirrored` and `isFlipped`—could be usefully conformed to `ValueEnumerable`. Why should we prevent that?
> * Having integer types conform to `ValueEnumerable` with `static let allValues = Self.min...Self.max` could be useful. Why should we prevent that?
> I'd say you're looking at it the wrong way. We're not *preventing* anything. We're adding a feature, and the question is, why should we *add* more than is justified by the use case?

Okay, here's the positive justification: The choice of an enum vs. a struct ought, to some degree, to be an implementation detail. As a general example of this, `__attribute__((swift_wrapper(enum)) ` types in Objective-C are actually imported into Swift as structs, but this detail rarely matters to users. A little closer to home, `Bool` could be an enum, but is implemented as a struct instead. `EncodingError` and `DecodingError` could be structs, but are implemented as enums instead.

To allow this flexibility, Swift rarely creates features for enums which are completely closed off to the structs (or vice versa), though they may have convenience features on only one of them. For example, both can have initializers, but only structs have them created implicitly; both can be RawRepresentable, but only enums get the sugar syntax. (The big exceptions are enum's pattern matching and struct's ability to encapsulate implementation details, but we've talked about bringing both of these features to the other side in some fashion.)

Therefore, I think this feature should follow the general Swift pattern and not be completely closed off to structs. It may not be as convenient to use there, but it should be possible. This preserves flexibility for type designers so they aren't forced to use enums merely because they want to use the standard mechanism for publishing the possible values of a type.

> Is there a clamor for enumerating the possible values of a type with two independently switchable `Bool`s? If so, is that not an argument to make `Bool` a valid raw value type? (There is already a bug report to make tuples of raw value types valid raw value types themselves.)

FWIW, I'm surprised Swift thinks "raw type 'Bool' is not expressible by any literal" when Bool is `ExpressibleByBooleanLiteral`. I'm not sure whether this is an oversight or if there's a specific reason for it.

> Why is it useful for (fixed-width) integer types to conform to `ValueEnumerable`? What use cases, exactly, would that enable that are not possible now?

It might permit advanced `ValueEnumerable` synthesis, for one thing. (But again, see my misgivings above about the usefulness of generating all possible permutations.)

> And at the same time, a small, specialized collection type _also_ helps with our intended use case in some ways (while admittedly making things more difficult in others). So I think the more general design, which also works better for our intended use case, is the superior option.
>> Along the lines of user ergonomics, I would advocate for as many enums as possible to conform without explicit opt-in. It's true that we are moving away from such magical designs, and for good reason, but the gain here of enums Just Working(TM) for such a long-demanded feature has, I would argue, more benefits than drawbacks. To my mind, the feature is a lot like `RawRepresentable` in several ways, and it would be defensible for an equal amount of magic to be enabled for it.
> But `RawRepresentable` *doesn't* get automatically added to all enums—you explicitly opt in, albeit using a special sugar syntax. No, I think opt-in is the right answer here. We might be able to justify adding sugar to opt-in, but I can't actually think of a way to make opting in easier than conforming to a protocol and letting the complier synthesize the requirements.
> Yes, you're right that `RawRepresentable` conformance *doesn't* get automatically added in, but there exists special sugar which makes the end result indistinguishable. By this I mean that the user gets `RawRepresentable` conformance without ever writing `Foo : RawRepresentable` anywhere (and neither do they write `Foo : Bar` where `Bar` is in turn `RawRepresentable`).

That is *not* getting conformance "without explicit opt-in". That is explicitly opting in through a sugar feature.

If you want to suggest a form of sugar which is substantially easier for users than adding a `ValueEnumerable` conformance clause, we're all ears. But I don't think there really is one. I don't think `@allValues enum Foo` is really enough of a win over `enum Foo: ValueEnumerable` to justify the additional language surface area.

> This is, in fact, a perfect opportunity to bring up a question I've been leaving implicit. Why not explore moving away from using a protocol? The proposed protocol has no syntactic requirements, and when constrained only the use case of enums, it has essentially no semantic requirements either. It seems that it's only because we've committed to using a protocol that we've opened up this exploration of what it means semantically to be `ValueEnumerable`, and how to generalize it to other types, and how to design the return type of the synthesized function, etc.
> What if some attribute would simply make the metatype conform to `Sequence` (or, for that matter, `BidirectionalCollection`)?

I would absolutely *adore* being able to conform metatypes to protocols, but I assume this would require major surgery to the type system. I also assume that the code generation needed to fulfill `RandomAccessCollection`'s requirements on `Foo.Type` would be much more complicated than generating a conformance. And there's also the problem that subscripts are currently not allowed as class/static members.

If it's actually feasible to modify the compiler with the features necessary to support this in the Swift 5 timeframe, I am totally willing to consider using `Foo.self` as the collection instead of `Foo.allValues`. But "give me a collection of all the cases" is a major convenience that users have been requesting for four years, and I really don't want to keep them waiting any longer just so we can deliver a Platonically ideal design.`Array(Foo.self)` would be pretty cool, but it's not *so* cool that we should delay something users will be ecstatic to have for several years just to get it.

Brent Royal-Gordon

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20171113/d0cdef92/attachment.html>

More information about the swift-evolution mailing list