Treating an Enum's Cases as Its Subtypes


(Niels Andriesse) #1

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.


(Matthew Johnson) #2

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 <http://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.

My value subtyping manifesto discusses enum case types in detail. You may find it very interesting: https://gist.github.com/anandabits/5b7f8e3836387e893e3a1197a4bf144d

In the manifesto I show how the author of an enum can give the type a name distinct from the case name. I also discuss “anonymous case types”. The are necessarily anonymous because we have no valid type identifier to use for them (but we could have access to them dynamically when we have a value of the case type).

···

On Feb 20, 2017, at 12:40 AM, Niels Andriesse via swift-evolution <swift-evolution@swift.org> wrote:

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Haravikk) #3

I really like the simplification of is/as here, since the leading dot should be clear enough and is consistent with other usages of enums.

Presumably this would apply even further though? For example, could I do:

  func myMethod(_ something:Foo.a) -> Foo.a { … }

i.e- take and return only instances of Foo.a?

It should then also be possible for me to do:

  let a:Foo = myMethod(Foo.a("test"))

In other words, Foo.a is a valid instance of Foo (but not vice versa without casting first)?

···

On 20 Feb 2017, at 06:40, Niels Andriesse via swift-evolution <swift-evolution@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 <http://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.


(Joe Groff) #4

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—remember that enums are sums, not unions, and that's important for composability and uniform behavior with generics. I would be fine allowing enum subtyping with some opt-in attribute, e.g.:

enum Optional<Wrapped> {
  sub case some(wrapped)
  case none
}

enum Result<Wrapped> {
  sub case ok(wrapped)
  case error(Error) // not a subtype
}

enum JSON {
  // OK for these to all be sub-cases, since they don't overlap
  sub case string(String), number(Double), array([JSON]), object([String: JSON]), null
}

enum Either<T, U> {
  // Error: sub cases potentially overlap
  sub case left(T), right(U)
}

-Joe

···

On Feb 20, 2017, at 7:32 AM, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:

On Feb 20, 2017, at 12:40 AM, Niels Andriesse via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@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 <http://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.


(Joe Groff) #5

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 <http://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.

—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.

-Joe

···

On Feb 20, 2017, at 1:04 PM, Matthew Johnson <matthew@anandabits.com> wrote:

On Feb 20, 2017, at 2:38 PM, Joe Groff <jgroff@apple.com <mailto:jgroff@apple.com>> wrote:

On Feb 20, 2017, at 7:32 AM, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Feb 20, 2017, at 12:40 AM, Niels Andriesse via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:


(Matthew Johnson) #6

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 <http://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.

For example let’s assume the numeric types have the desired subtype relationships and I have the following enum for which the cases are subtypes of the enum itself and are given the types of the associated value payloads. This example uses the syntax I used in my value subtyping manifesto for anonymous cases that are subtypes of the enum:

enum FixedInt {
   case -> Int8
   case -> Int16
   case -> Int32
   case -> Int64
}

let fixedInt = Int8(42) // resolves to `FixedInt.int8(42)`

Now imagine `FixedInt` does not include the `Int8` case:

let fixedInt = Int8(42) // resolves to `FixedInt.int16(42)`

I don’t see how this is different than:

func foo(_ int8: Int8) {}
func foo(_ int16: Int16) {}
func foo(_ int16: Int16) {}
func foo(_ int16: Int16) {}

foo(Int16(42))

As long as value subtyping follows the same overload resolution rules that are used for classes and protocol existentials the right thing happens.

If there is no case with the exact type is getting converted the nearest ancestor type is preferred. In the case of a non-linear type hierarchy (as we already have in protocols) if the overloads available to choose from do not form a strictly linear order *then* we do have ambiguity. In that case it would need to be resolve manually by explicitly specifying which case is intended.

This doesn’t seem like a problem to me - just ordinary overload resolution.

—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?

I would be fine allowing enum subtyping with some opt-in attribute, e.g.:

enum Optional<Wrapped> {
  sub case some(wrapped)
  case none
}

enum Result<Wrapped> {
  sub case ok(wrapped)
  case error(Error) // not a subtype
}

enum JSON {
  // OK for these to all be sub-cases, since they don't overlap
  sub case string(String), number(Double), array([JSON]), object([String: JSON]), null
}

enum Either<T, U> {
  // Error: sub cases potentially overlap
  sub case left(T), right(U)
}

Why can’t we defer the error? `Either<Int, String>` should be allowed but `Either<Int, Int>` should be an error.

Alternatively, if we agree that overlapping is allowed and overload resolution is used then we could introduce a `!=` constraint to verify correctness at the declaration of `Either`:

enum Either<T, U> where T != U { … }

···

On Feb 20, 2017, at 2:38 PM, Joe Groff <jgroff@apple.com> wrote:

On Feb 20, 2017, at 7:32 AM, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Feb 20, 2017, at 12:40 AM, Niels Andriesse via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

-Joe


(Matthew Johnson) #7

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 <http://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.

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.

···

On Feb 20, 2017, at 3:22 PM, Joe Groff <jgroff@apple.com> wrote:

On Feb 20, 2017, at 1:04 PM, Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:

On Feb 20, 2017, at 2:38 PM, Joe Groff <jgroff@apple.com <mailto:jgroff@apple.com>> wrote:

On Feb 20, 2017, at 7:32 AM, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Feb 20, 2017, at 12:40 AM, Niels Andriesse via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

-Joe


(Jon Hull) #8

Just playing devil’s advocate for a second:
What if, instead of having an explicit annotation, we make a simple rule to break ambiguity. That is, if the type is unambiguous, we use that, and if it is ambiguous, we use the first one (as defined by the creation/definition order). Then we have an explicit annotation to override that when it is wrong.

That said, I am on the fence about implicit conversion here. I really like the ‘is .a’ notation, but automatically promoting from String/Int/Etc… might be unexpected in the wrong context. I do want conversion in some cases, but as Joe says, there might be other cases where I would want to require explicit opt-in of some sort to enable implicit conversion. I could see that opt-in potentially being on the whole enum though (like ‘indirect’).

I am on the fence though… I could be convinced.

···

On Feb 20, 2017, at 12:38 PM, Joe Groff via swift-evolution <swift-evolution@swift.org> wrote:

On Feb 20, 2017, at 7:32 AM, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Feb 20, 2017, at 12:40 AM, Niels Andriesse via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@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 <http://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—remember that enums are sums, not unions, and that's important for composability and uniform behavior with generics. I would be fine allowing enum subtyping with some opt-in attribute, e.g.:

enum Optional<Wrapped> {
  sub case some(wrapped)
  case none
}

enum Result<Wrapped> {
  sub case ok(wrapped)
  case error(Error) // not a subtype
}

enum JSON {
  // OK for these to all be sub-cases, since they don't overlap
  sub case string(String), number(Double), array([JSON]), object([String: JSON]), null
}

enum Either<T, U> {
  // Error: sub cases potentially overlap
  sub case left(T), right(U)
}

-Joe
_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Joe Groff) #9

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 <http://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

···

On Feb 20, 2017, at 1:53 PM, Matthew Johnson <matthew@anandabits.com> wrote:

On Feb 20, 2017, at 3:22 PM, Joe Groff <jgroff@apple.com <mailto:jgroff@apple.com>> wrote:

On Feb 20, 2017, at 1:04 PM, Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:

On Feb 20, 2017, at 2:38 PM, Joe Groff <jgroff@apple.com <mailto:jgroff@apple.com>> wrote:

On Feb 20, 2017, at 7:32 AM, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Feb 20, 2017, at 12:40 AM, Niels Andriesse via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

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


(Patrick Pijnappel) #10

Just to clarify, the proposal doesn't suggest to allow the associated value
to be used as a subtype of the enum.

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@swift.org> wrote:

On Feb 20, 2017, at 1:53 PM, Matthew Johnson <matthew@anandabits.com> > wrote:

On Feb 20, 2017, at 3:22 PM, Joe Groff <jgroff@apple.com> wrote:

On Feb 20, 2017, at 1:04 PM, Matthew Johnson <matthew@anandabits.com> > wrote:

On Feb 20, 2017, at 2:38 PM, Joe Groff <jgroff@apple.com> wrote:

On Feb 20, 2017, at 7:32 AM, Matthew Johnson via swift-evolution < > swift-evolution@swift.org> wrote:

On Feb 20, 2017, at 12:40 AM, Niels Andriesse via swift-evolution < > swift-evolution@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Matthew Johnson) #11

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. 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
}

···

Sent from my iPad

On Feb 21, 2017, at 2:47 AM, Patrick Pijnappel <patrickpijnappel@gmail.com> wrote:

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@swift.org> wrote:

On Feb 20, 2017, at 1:53 PM, Matthew Johnson <matthew@anandabits.com> wrote:

On Feb 20, 2017, at 3:22 PM, Joe Groff <jgroff@apple.com> wrote:

On Feb 20, 2017, at 1:04 PM, Matthew Johnson <matthew@anandabits.com> wrote:

On Feb 20, 2017, at 2:38 PM, Joe Groff <jgroff@apple.com> wrote:

On Feb 20, 2017, at 7:32 AM, Matthew Johnson via swift-evolution <swift-evolution@swift.org> wrote:

On Feb 20, 2017, at 12:40 AM, Niels Andriesse via swift-evolution <swift-evolution@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Niels Andriesse) #12

Exactly

···

On Tue, 21 Feb 2017 at 19:50, Patrick Pijnappel via swift-evolution < swift-evolution@swift.org> wrote:

Just to clarify, the proposal doesn't suggest to allow the associated
value to be used as a subtype of the enum.

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@swift.org> wrote:

On Feb 20, 2017, at 1:53 PM, Matthew Johnson <matthew@anandabits.com> > wrote:

On Feb 20, 2017, at 3:22 PM, Joe Groff <jgroff@apple.com> wrote:

On Feb 20, 2017, at 1:04 PM, Matthew Johnson <matthew@anandabits.com> > wrote:

On Feb 20, 2017, at 2:38 PM, Joe Groff <jgroff@apple.com> wrote:

On Feb 20, 2017, at 7:32 AM, Matthew Johnson via swift-evolution < > swift-evolution@swift.org> wrote:

On Feb 20, 2017, at 12:40 AM, Niels Andriesse via swift-evolution < > swift-evolution@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Joe Groff) #13

I see. I would say that, like payload subtyping, having an independent type per case is sometimes useful, but not universally so. Result is in fact a good example of when you really don't want the cases to have independent type identity from the payloads, since there's really nothing interesting about "success" apart from T. "some" and "none" in an Optional or the different cases of JSON data similarly are just containers with no interesting meaning beyond being arms of an enum. If the cases are themselves types, that means they also need ABI layout rules, runtime type metadata, and other ceremony independent of the parent enum.

-Joe

···

On Feb 21, 2017, at 12:47 AM, Patrick Pijnappel <patrickpijnappel@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.

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>.


(Xiaodi Wu) #14

Sent from my iPad

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.

···

On Tue, Feb 21, 2017 at 6:56 AM, Matthew Johnson via swift-evolution < swift-evolution@swift.org> wrote:

On Feb 21, 2017, at 2:47 AM, Patrick Pijnappel <patrickpijnappel@gmail.com> > wrote:

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@swift.org> wrote:

On Feb 20, 2017, at 1:53 PM, Matthew Johnson <matthew@anandabits.com> >> wrote:

On Feb 20, 2017, at 3:22 PM, Joe Groff <jgroff@apple.com> wrote:

On Feb 20, 2017, at 1:04 PM, Matthew Johnson <matthew@anandabits.com> >> wrote:

On Feb 20, 2017, at 2:38 PM, Joe Groff <jgroff@apple.com> wrote:

On Feb 20, 2017, at 7:32 AM, Matthew Johnson via swift-evolution < >> swift-evolution@swift.org> wrote:

On Feb 20, 2017, at 12:40 AM, Niels Andriesse via swift-evolution < >> swift-evolution@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Matthew Johnson) #15

Sent from my iPad

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?

···

On Feb 21, 2017, at 2:35 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:
On Tue, Feb 21, 2017 at 6:56 AM, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
On Feb 21, 2017, at 2:47 AM, Patrick Pijnappel <patrickpijnappel@gmail.com <mailto:patrickpijnappel@gmail.com>> wrote:

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@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Feb 20, 2017, at 1:53 PM, Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:

On Feb 20, 2017, at 3:22 PM, Joe Groff <jgroff@apple.com <mailto:jgroff@apple.com>> wrote:

On Feb 20, 2017, at 1:04 PM, Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:

On Feb 20, 2017, at 2:38 PM, Joe Groff <jgroff@apple.com <mailto:jgroff@apple.com>> wrote:

On Feb 20, 2017, at 7:32 AM, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Feb 20, 2017, at 12:40 AM, Niels Andriesse via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@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 <http://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@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Niels Andriesse) #16

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@swift.org> wrote:

On Feb 21, 2017, at 2:35 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Tue, Feb 21, 2017 at 6:56 AM, Matthew Johnson via swift-evolution < > swift-evolution@swift.org> wrote:

Sent from my iPad

On Feb 21, 2017, at 2:47 AM, Patrick Pijnappel < >> patrickpijnappel@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@swift.org> wrote:

On Feb 20, 2017, at 1:53 PM, Matthew Johnson <matthew@anandabits.com> >>> wrote:

On Feb 20, 2017, at 3:22 PM, Joe Groff <jgroff@apple.com> wrote:

On Feb 20, 2017, at 1:04 PM, Matthew Johnson <matthew@anandabits.com> >>> wrote:

On Feb 20, 2017, at 2:38 PM, Joe Groff <jgroff@apple.com> wrote:

On Feb 20, 2017, at 7:32 AM, Matthew Johnson via swift-evolution < >>> swift-evolution@swift.org> wrote:

On Feb 20, 2017, at 12:40 AM, Niels Andriesse via swift-evolution < >>> swift-evolution@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Matthew Johnson) #17

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 <http://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 Feb 22, 2017, at 3:34 AM, Niels Andriesse <andriesseniels@gmail.com> wrote:

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

On Feb 21, 2017, at 2:35 PM, Xiaodi Wu <xiaodi.wu@gmail.com <mailto:xiaodi.wu@gmail.com>> wrote:

On Tue, Feb 21, 2017 at 6:56 AM, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Sent from my iPad

On Feb 21, 2017, at 2:47 AM, Patrick Pijnappel <patrickpijnappel@gmail.com <mailto:patrickpijnappel@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@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Feb 20, 2017, at 1:53 PM, Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:

On Feb 20, 2017, at 3:22 PM, Joe Groff <jgroff@apple.com <mailto:jgroff@apple.com>> wrote:

On Feb 20, 2017, at 1:04 PM, Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:

On Feb 20, 2017, at 2:38 PM, Joe Groff <jgroff@apple.com <mailto:jgroff@apple.com>> wrote:

On Feb 20, 2017, at 7:32 AM, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Feb 20, 2017, at 12:40 AM, Niels Andriesse via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@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 <http://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@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution


(Patrick Pijnappel) #18

Alternatively, we could still use the as? and is keywords. This would be
slightly different from their normal usage, but arguably this is unlikely
to lead to misunderstandings or confusion in this case - and they are nicer
as keywords.

···

On Wed, Feb 22, 2017 at 8:34 PM, Niels Andriesse via swift-evolution < swift-evolution@swift.org> 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.

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

On Feb 21, 2017, at 2:35 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Tue, Feb 21, 2017 at 6:56 AM, Matthew Johnson via swift-evolution < >> swift-evolution@swift.org> wrote:

Sent from my iPad

On Feb 21, 2017, at 2:47 AM, Patrick Pijnappel < >>> patrickpijnappel@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@swift.org> wrote:

On Feb 20, 2017, at 1:53 PM, Matthew Johnson <matthew@anandabits.com> >>>> wrote:

On Feb 20, 2017, at 3:22 PM, Joe Groff <jgroff@apple.com> wrote:

On Feb 20, 2017, at 1:04 PM, Matthew Johnson <matthew@anandabits.com> >>>> wrote:

On Feb 20, 2017, at 2:38 PM, Joe Groff <jgroff@apple.com> wrote:

On Feb 20, 2017, at 7:32 AM, Matthew Johnson via swift-evolution < >>>> swift-evolution@swift.org> wrote:

On Feb 20, 2017, at 12:40 AM, Niels Andriesse via swift-evolution < >>>> swift-evolution@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Niels Andriesse) #19

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 :slight_smile:

···

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

On Feb 22, 2017, at 3:34 AM, Niels Andriesse <andriesseniels@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@swift.org> wrote:

On Feb 21, 2017, at 2:35 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Tue, Feb 21, 2017 at 6:56 AM, Matthew Johnson via swift-evolution < >> swift-evolution@swift.org> wrote:

Sent from my iPad

On Feb 21, 2017, at 2:47 AM, Patrick Pijnappel < >>> patrickpijnappel@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@swift.org> wrote:

On Feb 20, 2017, at 1:53 PM, Matthew Johnson <matthew@anandabits.com> >>>> wrote:

On Feb 20, 2017, at 3:22 PM, Joe Groff <jgroff@apple.com> wrote:

On Feb 20, 2017, at 1:04 PM, Matthew Johnson <matthew@anandabits.com> >>>> wrote:

On Feb 20, 2017, at 2:38 PM, Joe Groff <jgroff@apple.com> wrote:

On Feb 20, 2017, at 7:32 AM, Matthew Johnson via swift-evolution < >>>> swift-evolution@swift.org> wrote:

On Feb 20, 2017, at 12:40 AM, Niels Andriesse via swift-evolution < >>>> swift-evolution@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@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution


(Matthew Johnson) #20

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.

We can get close to “any one of these cases” using subenums:

enum Foo {
    case one
    cases enum Bar {
        case two
        case three
    }
}

This obviously requires you to plan ahead and enforces a strict, non-overlapping hierarchy. You couldn’t also have a way to say “case one and case two but not case three”. This seems ok to me.

If you want the ability to create types like that on the fly you get into the territory of structural unions, user-declared subtype relationships, or a special case feature that allows you to specify an ad-hoc list of cases from an enum and treats that type as a subtype of the enum itself. We know Swift is not going to have structural unions. I think we need to be extremely careful about allowing user-declared subtype relationships. And a special case feature is not likely to provide enough value to be worth the complexity.

···

On Feb 22, 2017, at 4:44 PM, T.J. Usiyan <griotspeak@gmail.com> wrote:

On Wed, Feb 22, 2017 at 3:37 PM, Niels Andriesse via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@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 :slight_smile:

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

On Feb 22, 2017, at 3:34 AM, Niels Andriesse <andriesseniels@gmail.com <mailto:andriesseniels@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 <http://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@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Feb 21, 2017, at 2:35 PM, Xiaodi Wu <xiaodi.wu@gmail.com <mailto:xiaodi.wu@gmail.com>> wrote:

On Tue, Feb 21, 2017 at 6:56 AM, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Sent from my iPad

On Feb 21, 2017, at 2:47 AM, Patrick Pijnappel <patrickpijnappel@gmail.com <mailto:patrickpijnappel@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@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Feb 20, 2017, at 1:53 PM, Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:

On Feb 20, 2017, at 3:22 PM, Joe Groff <jgroff@apple.com <mailto:jgroff@apple.com>> wrote:

On Feb 20, 2017, at 1:04 PM, Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:

On Feb 20, 2017, at 2:38 PM, Joe Groff <jgroff@apple.com <mailto:jgroff@apple.com>> wrote:

On Feb 20, 2017, at 7:32 AM, Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

On Feb 20, 2017, at 12:40 AM, Niels Andriesse via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@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 <http://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@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution

_______________________________________________
swift-evolution mailing list
swift-evolution@swift.org <mailto:swift-evolution@swift.org>
https://lists.swift.org/mailman/listinfo/swift-evolution