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

Niels Andriesse andriesseniels at gmail.com
Wed Feb 22 14:37:51 CST 2017


>
> You’re allowing what is in many respects a downcast to something that
> behaves like a struct or tuple.  What type does it have?  Is it a tuple?
>

Yes, it would be a tuple in this case :)

On Thu, Feb 23, 2017 at 2:29 AM, Matthew Johnson <matthew at anandabits.com>
wrote:

>
> On Feb 22, 2017, at 3:34 AM, Niels Andriesse <andriesseniels at gmail.com>
> wrote:
>
> 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.
>
>
> I agree that we don’t always need a subtype.  In my example `Bar.bar` and
> `Foo.Nested. aCaseWithoutAnIndependentType` don’t have an explicit type
> and therefor there is not a subtype relationship.  The other cases in the
> example show various ways of forming subtype relationships where that is
> desired.
>
> You’re allowing what is in many respects a downcast to something that
> behaves like a struct or tuple.  What type does it have?  Is it a tuple?
> So asCase? is a special cast operator for downcasting an enum to a “case
> tuple”?
>
> The only difference between this and real subtyping that would work with
> `as?` is that you don’t have implicit conversion from a subtype to the enum
> type.  I can definitely see how this downcast only behavior could be useful
> in some cases and can’t imagine it every being harmful the way that
> implicit conversion to the enum could be in some cases.
>
> I view this type-less downcast to be potentially useful syntactic sugar
> but it doesn’t provide any real capabilities that we don’t already have.
> For this reason I would consider it out of scope for Swift 4.  Real
> subtyping on the other hand offers very powerful new capabilities.  It
> isn’t clear to me whether or not the core team views this as something they
> are willing to consider for Swift 4 or not, but it is definitely not in the
> realm of syntactic sugar so it has a better chance than `asCase?`.  Joe,
> any thoughts on whether it’s worth investing time into a proposal or not?
>
>
>
> 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/20170223/62c0995c/attachment.html>


More information about the swift-evolution mailing list