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

T.J. Usiyan griotspeak at gmail.com
Wed Feb 22 16:44:28 CST 2017


What if, at least to begin with, we don't infer anything with regard to
what is a subtype and what isn't? Clear diagnostics could be given when the
compiler spots a potential conflict. I would also like to suggest that we
think of a way to spell "any one of these cases" for closure
parameters/return types.

On Wed, Feb 22, 2017 at 3:37 PM, Niels Andriesse via swift-evolution <
swift-evolution at swift.org> wrote:

> 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
>>>
>>>
>>
>>
>
> _______________________________________________
> 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/880544f5/attachment.html>


More information about the swift-evolution mailing list