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

Xiaodi Wu xiaodi.wu at gmail.com
Tue Feb 21 14:35:31 CST 2017


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.

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.


> 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
>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170221/0a60c392/attachment.html>


More information about the swift-evolution mailing list