[Proposal] Powerful enumerations built upon protocols, structures and classes


(Marc Knaup) #1

Dear community,

after working around several limitations of enumerations I had the thought
that it should be possible to represent their behavior using just
protocols, structures and classes.

Such an approach would have several benefits:

   - The enum/case syntax would simply be syntactic sugar for constructs
   you can also create using protocols, structures and classes which
   simplifies the type system since enumerations are no longer something
   special.
   - Enumerations gain a lot of additional functionality currently
   available only to protocols, structures and classes.
   - Enumerations and their cases have a less stronger coupling, allowing
   you for example to add an existing type as an additional case.
   - It becomes possible to refer to an enumeration case directly since
   it's now a type.
   - For the same reason enumeration cases can be constructed and passed
   around independently from their enumeration.
   - The same enumeration case could be used by multiple enumerations.
   - Enumerations can have very flexible initializers instead of just the
   default one for tuples when using Associated Values.
   - Enumerations defined in Swift 2.1 will still work with minimal
   migration.

On the other hand though there are still several open issues preventing
such an approach from being implementable. Most of them stem from current
limitations of protocols and generics as well as the lack of a
singleton-like construct for structures.

Since the proposal is rather extensive I already wrote an initial version
which outlines the changes, explains the pros and cons, solves as many
issues as possible already and lists all remaining issues to be solved.

The proposal also suggests extensions in other areas of the language in
order to overcome the several limitations of the language. These changes
may or may not be desirable and must also be evaluated carefully. If the
discussion indicates that these changes are also a good idea, then they
will be spun out to their own proposal making this one dependent on them.

I would love to hear your feedback and ideas for improvements.

https://github.com/fluidsonic/swift-evolution/blob/master/proposals/NNNN-powerful-enumerations-built-upon-protocols-structures-and-classes.md

Thank you,
  Marc


(Al Skipp) #2

Sum types look very interesting and are very similar to enumerations.
If there is enough interest in sum types then this proposal could probably cover them too since it already provides a good foundation for implementing them.
It could also be proposed separately or on top of this proposal.
In any case this proposal does not prevent sum types from being added to Swift.

Swift already has Sum types, they are enums. Pretty much all the other constructs are Product types (structs, classes, tuples…). I’ve not managed to read your detailed proposal in full, but it strikes me that it would involve quite fundamental changes. Not sure what all the consequences would be and what the value is in blurring the distinction between Sum types and Product types?

My personal view is that the distinction between enums and protocols is important. Your proposal would enable the easy creation of extensible enums which I find problematic. When using extensible enums you’d be obliged to include a ‘default’ case in every switch statement. You point out that you’d be able to restrict the cases by using ‘final’ or some other means, but in practice I think the extensible form would be popular for its perceived flexibility.

Currently if I create an enum I am obliged to deal with all cases in a switch (assuming I don’t use a default case, which I try to avoid). Now imagine I subsequently realise I need to add a case to the enum. OK, I do so, now all my code breaks, arghh! But this is a good thing! The compiler guides me to every piece of code I need to change – magic! If on the other hand, enums were easily extensible, I’d add my new case and my code would still compile, brilliant! However, my code is fundamentally broken and I have to manually fix it without any guidance from the compiler.

That’s why I think the distinction between enums and protocols is important - enums are useful because they’re not extensible.
When a type is required that can be extended with new cases, then a protocol is the right tool for the job, rather than an enum.

My worry is this proposal would reduce compile time guarantees in Swift and make me more responsible for finding my own bugs (the horror!).

Al


(Marc Knaup) #3

Thank you for your extensive feedback.

The way I proposed it is actually the other way round:
Switches over enumerations still don't need a default case since all cases
(i.e. types conforming to them) are known at compile-time.
The same benefit will become available also to other protocols. If a
protocol is not public (or not publicly conformable) then the compiler
again knows all types explicitly declaring conformance to that protocol at
compile-time and the developer can omit a default case when switching over
instances of that protocol.

So the behavior of Swift 2.1 regarding switches and default cases for
enumerations remains the same while for many protocols the compiler can
additionally issue a warning that the default case is unreachable and thus
unnecessary.

···

On Tue, Dec 15, 2015 at 2:59 PM, Al Skipp <al_skipp@fastmail.fm> wrote:

> Sum types look very interesting and are very similar to enumerations.
> If there is enough interest in sum types then this proposal could
probably cover them too since it already provides a good foundation for
implementing them.
> It could also be proposed separately or on top of this proposal.
> In any case this proposal does not prevent sum types from being added to
Swift.

Swift already has Sum types, they are enums. Pretty much all the other
constructs are Product types (structs, classes, tuples…). I’ve not managed
to read your detailed proposal in full, but it strikes me that it would
involve quite fundamental changes. Not sure what all the consequences would
be and what the value is in blurring the distinction between Sum types and
Product types?

My personal view is that the distinction between enums and protocols is
important. Your proposal would enable the easy creation of extensible enums
which I find problematic. When using extensible enums you’d be obliged to
include a ‘default’ case in every switch statement. You point out that
you’d be able to restrict the cases by using ‘final’ or some other means,
but in practice I think the extensible form would be popular for its
perceived flexibility.

Currently if I create an enum I am obliged to deal with all cases in a
switch (assuming I don’t use a default case, which I try to avoid). Now
imagine I subsequently realise I need to add a case to the enum. OK, I do
so, now all my code breaks, arghh! But this is a good thing! The compiler
guides me to every piece of code I need to change – magic! If on the other
hand, enums were easily extensible, I’d add my new case and my code would
still compile, brilliant! However, my code is fundamentally broken and I
have to manually fix it without any guidance from the compiler.

That’s why I think the distinction between enums and protocols is
important - enums are useful because they’re not extensible.
When a type is required that can be extended with new cases, then a
protocol is the right tool for the job, rather than an enum.

My worry is this proposal would reduce compile time guarantees in Swift
and make me more responsible for finding my own bugs (the horror!).

Al


(John McCall) #4

It’s not obvious to me that allowing exhaustive switches over ordinary private/internal protocols is really a good language design. It’s generally not a source-breaking change to make a declaration more accessible; and that’s a very good thing, because reorganizing a module to turn some of its internal abstractions into external ones (perhaps by splitting the module) is a natural code-evolution step.

Anyway, your proposal is very long and changes a lot of things, so I’m afraid that my responses will be relatively high-level, and I’m sorry if you’ve already considered some of these points. In general, I think this is a very interesting idea, but it’s a very significant shift to the language model, and I’m concerned about some of the implications.

1. This doesn’t really simplify the fundamental type system. Enums are a kind of possibly-generic nominal type that lack subtype conversions; in this sense, they exactly mirror structs. We could have a hundred different formal kinds of possibly-generic nominal types that lack subtype conversions and it would all be basically the same to the fundamental type system.

2. There are subsidiary features required for the convenience of enums, like exhaustiveness checking. These features still exist in your proposal. So still no simplification.

3. Since the enum form will still exist in the language, and since the design patterns using it will typically be quite different from those for protocols, it’s not really a simplification for the programming model, either.

4. Generic enums will become completely unusable unless we lift the associated-type restrictions on existentials. This is something we’re interested in doing anyway, but it’s worth noting that it’s a prerequisite for this proposal.

5. Even if (4) is addressed, generic enums will take a massive usability hit unless we find a more convenient syntax for existential-type-with-specified-associated-type (e.g. Collection<T>). Again, this is something we may be interested in anyway. This, however, imposes specific constraints on the general solution that we might be less comfortable with.

6. Your proposal subtly adds a major new expressive power to protocols. In particular, things like CompassPoint.North mean that there are now concrete declarations inside protocols, not just protocol requirements.

7. The change to the inferred type of CompassPoint.North seems pretty bad to me. Consider a “var” instead of a “let”.

8. Types normally cause the compiler to emit a fair amount of global metadata. I’m concerned about spitting out a ton of basically useless metadata for, say, a C-like enum with twenty cases.

9. Enums have specialized representations in memory which only allocate space for the enumerated cases; let’s call this a “tagged-sum” representation, as opposed to the standard “opaque” representation used by existentials. Implicitly using a tagged-sum representation for a sealed protocol requires a lot of heroism from the compiler, and this could become a compile-time performance bottleneck and, in general, a major obstacle for incremental compilation.

10. It is unclear to me what happens to the tagged-sum representation in, say, a protocol composition type. If the answer is to use a representation which only includes the intersection of the protocols, that implies the use of potentially expensive dynamic representation changes when performing a subtype conversion between different existential types. That conversion is something that can happen dynamically due to dynamic casts in generic code.

John.

···

On Dec 15, 2015, at 6:50 AM, Marc Knaup via swift-evolution <swift-evolution@swift.org> wrote:
Thank you for your extensive feedback.

The way I proposed it is actually the other way round:
Switches over enumerations still don't need a default case since all cases (i.e. types conforming to them) are known at compile-time.
The same benefit will become available also to other protocols. If a protocol is not public (or not publicly conformable) then the compiler again knows all types explicitly declaring conformance to that protocol at compile-time and the developer can omit a default case when switching over instances of that protocol.

So the behavior of Swift 2.1 regarding switches and default cases for enumerations remains the same while for many protocols the compiler can additionally issue a warning that the default case is unreachable and thus unnecessary.