[swift-evolution] Treating an Enum's Cases as Its Subtypes

Niels Andriesse andriesseniels at gmail.com
Wed Feb 22 03:34:07 CST 2017


I agree with Joe that it's not always useful for enum cases to have
independent type identity, and that it would probably also mean quite a bit
of overhead if it were implemented.

That said, the main focus of my original proposal was to simplify enum
handling (specifically, to make it syntactically more similar to the
handling of other types).

If we do away with the idea of making enum cases subtypes of the enum for
now, and implement something like the syntax below, that might still beat
the current "if case" syntax and make enum handling simpler.

enum Foo {
  case a(name: String)
}

foo isCase .a(name:)
if let a = foo asCase? .a(name:) { print(a.name) } // .a(name:) is not a
problem because it's not a type name

Note that we're binding to the associated value of the enum case and that
the enum case is not a subtype of the enum.

You could argue that it's syntactic sugar, but it might improve enum
handling without adding too much overhead.


On Wed, Feb 22, 2017 at 7:50 AM, Matthew Johnson via swift-evolution <
swift-evolution at swift.org> wrote:

>
> On Feb 21, 2017, at 2:35 PM, Xiaodi Wu <xiaodi.wu at gmail.com> wrote:
>
> On Tue, Feb 21, 2017 at 6:56 AM, Matthew Johnson via swift-evolution <
> swift-evolution at swift.org> wrote:
>
>>
>>
>> Sent from my iPad
>>
>> On Feb 21, 2017, at 2:47 AM, Patrick Pijnappel <
>> patrickpijnappel at gmail.com> wrote:
>>
>> Just to clarify, the proposal doesn't suggest to allow the associated
>> value to be used as a subtype of the enum.
>>
>>
>> Understood.  But it's also very desirable to have the type of the
>> associated value be a subtype of the enum in some cases, as we already have
>> with Optional today.
>>
>
> FWIW, I agree with you here that I'd find it more useful to have the _type
> of the associated value_ be a subtype of the enum than to have the case
> itself be an independent type that is a subtype of the enum.
>
>
> There are cases where both of these are valuable, but I agree with you -
> if I had to pick one I would pick the type of the associated value for sure.
>
>
> With respect to the latter, Swift 3 actually lowercased enum cases on the
> premise that they should *not* be treated as independent types. To reverse
> direction now (as others have mentioned in threads on other topics) partly
> calls into question the evolution process itself; a consensus of the
> community and core team has already been declared.
>
>
> In my value subtyping manifesto I suggested allowing them to have
> synthesized structs backing them when they are treated as independent types
> by assigning the type a name that is independent of the case name.  Cases
> with a single associated value may have a type that matches the type of the
> associated value, and may even be anonymous.  I also allowed sub enums to
> be declared which are also subtypes of the enum itself.
>
> Enum Bar {
>     case bar
> }
> enum Foo {
>    case one -> struct One
>    case two(name: String) -> struct Two
>    case three(Int) -> Int
>    case -> String
>
>    cases Bar // makes Bar a subtype of Foo and exposes the cases directly
> on Foo
>
>    cases enum Nested { // this is like any other nested enum, but is also
> a subtype of Foo and the cases are directly available on Foo
>        case aCaseWithoutAnIndependentType
>    }
> }
>
> This is the kind of system I would like to see for enum subtypes.  If it
> has a chance of being accepted for Swift 4 I would be very happy to write a
> proposal.  Can anyone from the core team comment on whether enum subtypes
> might be in scope?
>
>
>
>> Result.success is a good example of when we would want this for the same
>> reason it is valuable in Optional.some.
>>
>> I would also like to see nested enums that are subtypes of the parent
>> enum.
>>
>> Inline:
>>
>> enum Foo {
>>    sub enum Bar {
>>        case one
>>        case two
>>    }
>>    case three
>> }
>>
>> And also wrapping an external enum:
>>
>> enum Bar {
>>        case one
>>        case two
>>  }
>> enum Foo {
>>     // this syntax is ambiguous - we need a way to differentiate an
>> inline sub enum from wrapping an existing enum
>>    sub enum Bar
>>    case three
>> }
>>
>>
>> enum Result<T> { case .success(T), .error(Error) }
>>
>> func foo(_ x: Result<Int>) { /* ... */ }
>> func bar(_ x: Result<Int>.success) { /* ... */ }
>>
>> // Not this:
>> foo(5)
>> bar(5)
>> // But rather:
>> foo(.success(5))
>> bar(.success(5))
>>
>> Effectively, Result<T>.success would behave like a struct that is a
>> subtype of Result<T>.
>>
>>
>> On Tue, Feb 21, 2017 at 12:50 PM, Joe Groff via swift-evolution <swift-
>> evolution at swift.org> wrote:
>>
>>>
>>> On Feb 20, 2017, at 1:53 PM, Matthew Johnson <matthew at anandabits.com>
>>> wrote:
>>>
>>>
>>> On Feb 20, 2017, at 3:22 PM, Joe Groff <jgroff at apple.com> wrote:
>>>
>>>
>>> On Feb 20, 2017, at 1:04 PM, Matthew Johnson <matthew at anandabits.com>
>>> wrote:
>>>
>>>
>>> On Feb 20, 2017, at 2:38 PM, Joe Groff <jgroff at apple.com> wrote:
>>>
>>>
>>> On Feb 20, 2017, at 7:32 AM, Matthew Johnson via swift-evolution <
>>> swift-evolution at swift.org> wrote:
>>>
>>>
>>> On Feb 20, 2017, at 12:40 AM, Niels Andriesse via swift-evolution <
>>> swift-evolution at swift.org> wrote:
>>>
>>> I'd like to discuss the possibility of treating the cases of a given
>>> enum as if they are subtypes of that enum. This seems like a natural thing
>>> to do because enum cases (especially when they have associated values)
>>> effectively define a closed set of subtypes.
>>>
>>> Doing so would allow for constructions such as the following:
>>>
>>> enum Foo {
>>>   case a(name: String)
>>> }
>>>
>>> func isA(foo: Foo) -> Bool {
>>>   // The old way:
>>>   if case .a = foo { return true }
>>>   return false
>>>   // The new way:
>>>   return foo is .a
>>> }
>>>
>>> func printNameIfFooIsA(foo: Foo) -> Bool {
>>>   // The old way:
>>>   if case let .a(name) = foo {
>>>     print(name)
>>>   }
>>>   // The new way (1):
>>>   if let a = foo as? .a {
>>>     print(a.name)
>>>   }
>>>   // The new way (2):
>>>   if let name = (foo as? .a)?.name {
>>>     print(name)
>>>   }
>>> }
>>>
>>> Treating an enum's cases as its subtypes would make enums easier to work
>>> with because handling them would be syntactically the same as handling
>>> other types.
>>>
>>> The pattern matching capabilities of enums wouldn't be affected by this
>>> proposal.
>>>
>>> Multiple other proposals have already attempted to simplify enum
>>> handling (they have particularly focused on getting rid of "if case" and
>>> adding the ability to treat enum case tests as expressions), but none of
>>> the solutions presented in those proposals have worked out so far.
>>>
>>> I believe that this could be the right solution to multiple enum-related
>>> problems that have been brought up repeatedly.
>>>
>>>
>>> I would like to see enum cases treated as subtypes of the enum type.
>>> This is an interesting way to refer to the type of a case.  Unfortunately I
>>> don’t think it will work if we accept the proposal to give cases a compound
>>> name.  If we do that the name of this case becomes `a(name:)` which is not
>>> a valid type name.
>>>
>>>
>>> I think there are definitely places where having cases be a subtype of
>>> an enum make sense, but I don't think it makes sense for *all* cases to be
>>> subtypes. For example, with "biased" containers like Optional and Result,
>>> it makes sense for the "right" side to be a subtype and the "wrong" side to
>>> be explicitly constructed, IMO.  If the types of cases overlap, it would
>>> also be *ambiguous* which case ought to be constructed when the payload is
>>> converted to the enum type
>>>
>>>
>>> Identical case types would definitely be a problem but I don’t think
>>> overlapping case types are always a problem.  I imagine this conversion
>>> working the same as any other ordinary overload resolution for ad-hoc
>>> overloads.
>>>
>>>
>>> Conversions happen at runtime too. `0 as Any as? Either<Int, Int>`
>>> wouldn't have any way to tell what `Either` to form if both arms of the
>>> Either were subtype candidates. An Either<T, U> in <T, U> context can end
>>> up being bound to Either<Int, Int> at runtime and interacting with runtime
>>> casts that way.
>>>
>>>
>>> Hmm.  This is unfortunate.
>>>
>>> In cases where T and U overlap and form a linear hierarchy but are not
>>> identical couldn’t the runtime determine the most direct path and choose
>>> that?
>>>
>>> If the compiler prohibited cases with exactly the same types like
>>> `Either<Int, Int>` from being expressed statically how do these types end
>>> up getting formed dynamically?  Is there any way those operations could be
>>> failable?
>>>
>>>
>>>
>>>
>>> —remember that enums are sums, not unions, and that's important for
>>> composability and uniform behavior with generics.
>>>
>>>
>>> I’ve always thought of enums as nominal discriminated unions.  Maybe I’m
>>> using the wrong terminology.  Can you elaborate on the difference between
>>> sums and unions?  When you say union are you talking about the kind of
>>> thing some people have brought up in the past where any members in common
>>> are automatically made available on the union type?
>>>
>>>
>>> Sums maintain structure whereas unions collapse it. As a sum,
>>> Optional<T> maintains its shape even when T = Optional<U>. If it were a
>>> union, T u Nil u Nil would collapse to T u Nil, losing the distinction
>>> between the inner and outer nil and leading to problems in APIs that use
>>> the outer nil to communicate meaning about some outer structure, such as
>>> asking for the `first` element of a collection of Optionals.
>>>
>>>
>>> Got it.  This is certainly a problem for `Optional`.
>>>
>>> But sometimes this behavior of collapsing the syntactic specification to
>>> a canonical sum type would be very useful.  What is the reason we can’t
>>> have something syntactic type expressions like `Int | String`, `Int |
>>> String | String, `String | Int | String | Int`, etc all collapse to the
>>> same canonical structural sum type:
>>>
>>> enum {
>>>    sub case int(Int), string(String)
>>> }
>>>
>>> This is how I’ve been thinking about those syntactic types.  We already
>>> allow existential types to be formed using syntax that collapses to a
>>> canonical type:
>>>
>>> typealias Existential1 = Protocol1 & Protocol2
>>> typealias Existential2 = Protocol2 & Existential1 & Protocol 3 &
>>> Protocol1
>>> typealias Existential3 = Existential1 & Protocol3
>>>
>>> In this example Existential1 and Existential3 are different names for
>>> the same type.
>>>
>>> Is there a reason we can’t have similar syntax that collapses to a
>>> similarly canonical sum type?  If we’re going to allow case subtypes this
>>> feels to me like a very natural and useful direction.
>>>
>>>
>>> A couple reasons that come to mind:
>>>
>>> - Most directly, we don't allow abstraction over generic constraints.
>>> `ExistentialN<T, U> = T & U` isn't allowed. As soon as you have abstraction
>>> over either unions or intersections, type checking becomes an unbounded
>>> search problem in the worst case, since every T binding is potentially
>>> equivalent to a T1 & T2 or T1 | T2 with T1 == T2 == T.
>>>
>>> - Sums and unions both imply a matching branch structure in the code
>>> somewhere to handle both possibilities. If the number of actual
>>> possibilities is different in different situations, that's a source of
>>> bugs, such as the overloading of `nil` I mentioned previously. Even if you
>>> did allow generic T & T types, the worst result of someone seeing that as
>>> T1 & T2 is that the operations enabled through conforming to T1 and T2 map
>>> to the same conformance.
>>>
>>> -Joe
>>>
>>>
>>> If we don’t allow it there are two problems: people have to invent a
>>> largely meaningless name for the enum and it is incompatible with any other
>>> similarly structured enum.  Neither is a significant problem but they do
>>> add (seemingly) unnecessary friction to the language.
>>>
>>> I wouldn’t expect these to be widely used - they would play a similar
>>> role as tuples - but they would be very appreciated where they are used.
>>>
>>>
>>> -Joe
>>>
>>>
>>>
>>> _______________________________________________
>>> swift-evolution mailing list
>>> swift-evolution at swift.org
>>> https://lists.swift.org/mailman/listinfo/swift-evolution
>>>
>>>
>>
>> _______________________________________________
>> swift-evolution mailing list
>> swift-evolution at swift.org
>> https://lists.swift.org/mailman/listinfo/swift-evolution
>
>
>
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org
> https://lists.swift.org/mailman/listinfo/swift-evolution
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170222/1625ff91/attachment.html>


More information about the swift-evolution mailing list