[Pre-Proposal-Discussion] Union Type - Swift 4

I'm a +1 for union types.

My main reason for wanting it is to eliminate (some) function overloads; behind the scenes the compiler may still produce one compiled function per union type (for performance), but at a high level we only need to worry about one implementation, and one call signature, which I think is a good thing.

···

On 11 Aug 2016, at 02:28, Cao Jiannan via swift-evolution <swift-evolution@swift.org> wrote:

Hi all,

I want to make a discussion about union type for swift 4.
See https://github.com/frogcjn/swift-evolution/blob/master/proposals/xxxx-union-type.md

Add union type grammar, represents the type which is one of other types.

var stringOrURL: String | URL = "https://www.apple.com <https://www.apple.com/&gt;&quot;
Now, if we using the new union type feature, we can declare type conveniently, No other type declaration, and compiler will automatically calculate the common interface.

func input(value: A | B | C) {
    print(value.commonProperty) // type checker will calculate the common interface, developer just use it out of box
    switch value {
    case let value as A:
        // value is type A
        print(value.propertyInA)
    case let value as B:
        // value is type B
        print(value.propertyInB)
    case let value as C:
        // value is type C
        print(value.propertyInC)
    }
    // there is no default case other than A, B or C. we already declared that.
}
Note: A, B, C can be either class or protocol, or any other types. This leaves developer more freedom.

Impact on existing code

This is a new feature, developer who need declare common type will alter to this new grammar.
Enum based version optional or IUO will be replaced by Union-based ones. Any optional type will automatically replaced by union type

<https://github.com/frogcjn/swift-evolution/blob/master/proposals/xxxx-union-type.md#detailed-design&gt;\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
swift-evolution mailing list
swift-evolution@swift.org
https://lists.swift.org/mailman/listinfo/swift-evolution

As an aside, you could use an enum instead of a protocol to avoid the problem of not having exhaustive switches:

enum GeometryValue {
  case point(Point)
  case line(Line)
}

And perhaps (depending on circumstances) you might not even need a Point and Line struct, so you could just put those values inside the enum:

enum GeometryValue {
  case point(x: Float, y: Float)
  case line(x: Float, y: Float, angle: Float)
}

However personally, I think using a protocol is the more Swifty approach to this problem. If I end up not having any common properties or functions that apply to that one umbrella protocol, then I consider that a signal that I’m modeling my solution incorrectly.

l8r
Sean

···

On Aug 12, 2016, at 6:24 AM, Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org> wrote:

Hi Cao,

I would be in favor until I find another approach to this problem:

Consider you have a geometry framework and two types: Point and Line

An intersection between two lines can be either none, a point or a line (if both are identical).

The return type would probably be (Point | Line)?

I've modeled it with an empty protocol "GeometryType". However this has a major disadvantage:
If you have a general "GeometryType?" you have to cast it in a switch to the specific type.
In case of (Point| Line)? the switch statement can be checked for exhaustiveness.

For future directions:

There should also be a subtype relationship:

let tu: (T | U) = T()
let tuv: (T | U | V) = tu // works

Overloaded functions/operators could also take Union types based on their overloads:

func take(_ i: Int) -> String { ... }

func take(_ s: String) -> Int? { ... }

let value: (Int | String) = "1234"
let value2 = take(value) // returns (String | Int?)

Best regards
Maximilian

Am 11.08.2016 um 03:28 schrieb Cao Jiannan via swift-evolution <swift-evolution@swift.org>:

Hi all,

I want to make a discussion about union type for swift 4.
See https://github.com/frogcjn/swift-evolution/blob/master/proposals/xxxx-union-type.md

Add union type grammar, represents the type which is one of other types.

var stringOrURL: String | URL = "https://www.apple.com"
Now, if we using the new union type feature, we can declare type conveniently, No other type declaration, and compiler will automatically calculate the common interface.

func input(value: A | B |
C) {
    
print(value.commonProperty) // type checker will calculate the common interface, developer just use it out of box

switch
value {
    
case let value as
A:
        
// value is type A

print(value.
propertyInA)
    
case let value as
B:
        
// value is type B

print(value.
propertyInB)
    
case let value as
C:
        
// value is type C

print(value.
propertyInC)
    }
    
// there is no default case other than A, B or C. we already declared that.

}

Note: A, B, C can be either class or protocol, or any other types. This leaves developer more freedom.

Impact on existing code

  • This is a new feature, developer who need declare common type will alter to this new grammar.
  • Enum based version optional or IUO will be replaced by Union-based ones. Any optional type will automatically replaced by union type

_______________________________________________
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

I considered the enum approach but it is very tedious to use "space.add(.point(point))" which also doesn't add any clarity.
The other approach where we add the properties of a Point as associated values also has a major drawback:

// you cannot create a point without "erasing" it's type
let point = GeometryValue.point(x: 4, y: 5)

// if you already have a point instance
"space.add(.point(x: point.x, y: point.y))"

If the union type feature is implemented the intersection of a Point and a Line returns (Point | Line) which is a subtype of a general GeometryValue eg. (Point | Line | Plane) and additionally cannot be checked for a Plane since it doesn't make sense.

The problem with a protocol is that there are only a few methods in common and again in a method like intersect there is no exhaustiveness check in switches.

Best regards
Maximilian

···

Am 12.08.2016 um 17:16 schrieb Sean Heber <sean@fifthace.com>:

As an aside, you could use an enum instead of a protocol to avoid the problem of not having exhaustive switches:

enum GeometryValue {
case point(Point)
case line(Line)
}

And perhaps (depending on circumstances) you might not even need a Point and Line struct, so you could just put those values inside the enum:

enum GeometryValue {
case point(x: Float, y: Float)
case line(x: Float, y: Float, angle: Float)
}

However personally, I think using a protocol is the more Swifty approach to this problem. If I end up not having any common properties or functions that apply to that one umbrella protocol, then I consider that a signal that I’m modeling my solution incorrectly.

l8r
Sean

On Aug 12, 2016, at 6:24 AM, Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org> wrote:

Hi Cao,

I would be in favor until I find another approach to this problem:

Consider you have a geometry framework and two types: Point and Line

An intersection between two lines can be either none, a point or a line (if both are identical).

The return type would probably be (Point | Line)?

I've modeled it with an empty protocol "GeometryType". However this has a major disadvantage:
If you have a general "GeometryType?" you have to cast it in a switch to the specific type.
In case of (Point| Line)? the switch statement can be checked for exhaustiveness.

For future directions:

There should also be a subtype relationship:

let tu: (T | U) = T()
let tuv: (T | U | V) = tu // works

Overloaded functions/operators could also take Union types based on their overloads:

func take(_ i: Int) -> String { ... }

func take(_ s: String) -> Int? { ... }

let value: (Int | String) = "1234"
let value2 = take(value) // returns (String | Int?)

Best regards
Maximilian

Am 11.08.2016 um 03:28 schrieb Cao Jiannan via swift-evolution <swift-evolution@swift.org>:

Hi all,

I want to make a discussion about union type for swift 4.
See https://github.com/frogcjn/swift-evolution/blob/master/proposals/xxxx-union-type.md

Add union type grammar, represents the type which is one of other types.

var stringOrURL: String | URL = "https://www.apple.com"
Now, if we using the new union type feature, we can declare type conveniently, No other type declaration, and compiler will automatically calculate the common interface.

func input(value: A | B |
C) {

print(value.commonProperty) // type checker will calculate the common interface, developer just use it out of box

switch
value {

case let value as
A:

// value is type A

print(value.
propertyInA)

case let value as
B:

// value is type B

print(value.
propertyInB)

case let value as
C:

// value is type C

print(value.
propertyInC)
   }

// there is no default case other than A, B or C. we already declared that.

}

Note: A, B, C can be either class or protocol, or any other types. This leaves developer more freedom.

Impact on existing code

   • This is a new feature, developer who need declare common type will alter to this new grammar.
   • Enum based version optional or IUO will be replaced by Union-based ones. Any optional type will automatically replaced by union type

_______________________________________________
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

But you can do that already with protocols, can't you?

···

On Fri, Aug 19, 2016 at 2:24 AM Haravikk via swift-evolution < swift-evolution@swift.org> wrote:

I'm a +1 for union types.

My main reason for wanting it is to eliminate (some) function overloads;
behind the scenes the compiler may still produce one compiled function per
union type (for performance), but at a high level we only need to worry
about one implementation, and one call signature, which I think is a good
thing.

On 11 Aug 2016, at 02:28, Cao Jiannan via swift-evolution < > swift-evolution@swift.org> wrote:

Hi all,

I want to make a discussion about union type for swift 4.
See
https://github.com/frogcjn/swift-evolution/blob/master/proposals/xxxx-union-type.md

Add union type grammar, represents the type which is one of other types.

var stringOrURL: String | URL = "https://www.apple.com"

Now, if we using the new union type feature, we can declare type
conveniently, No other type declaration, and compiler will automatically
calculate the common interface.

func input(value: A | B | C) {
    print(value.commonProperty) // type checker will calculate the common interface, developer just use it out of box
    switch value {
    case let value as A:
        // value is type A
        print(value.propertyInA)
    case let value as B:
        // value is type B
        print(value.propertyInB)
    case let value as C:
        // value is type C
        print(value.propertyInC)
    }
    // there is no default case other than A, B or C. we already declared that.
}

Note: A, B, C can be either class or protocol, or any other types. This
leaves developer more freedom.

Impact on existing code

   - This is a new feature, developer who need declare common type will
   alter to this new grammar.
   - Enum based version optional or IUO will be replaced by Union-based
   ones. Any optional type will automatically replaced by union type

<https://github.com/frogcjn/swift-evolution/blob/master/proposals/xxxx-union-type.md#detailed-design&gt;
_______________________________________________
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

When dealing with your own types, sure, but if you're talking about standard types or types from other libraries then it would mean extending them with whatever it is you actually need, which I'm not sure is the best way to do it. Type unions are much simpler, and keep all of the code within a single function.

To give a simplistic example, consider a method for adding a property to a property list file. I'm going to trim this down considerably, but it'll give an idea hopefully:

  func addProperty(key:String, value:Int | String, file:NSFile) {
    var xml = "<key>\(key)</key>\n"
    switch value {
      case value as Int:
        xml += "<number>\(value)</number>\n"
      case value as String:
        xml += "<string>\(value)</string>\n"
    }

    // Actually store the xml string in the file here
  }

Currently you might instead do this like:

  func addProperty(key:String, value:Int, file:NSFile) {
    addProperty(key: key, rawValue: "<number>\(value)</number>", file: file)
  }
  func addProperty(key:String, value:String, file:NSFile) {
    addProperty(key: key, rawValue: "<string>\(value)</string>", file: file)
  }
  func addProperty(key:String, rawValue:String, file:NSFile) {
    var xml = "<key>\(key)</key>\n\(rawValue\)\n"
    // Actually store the xml string in the file here
  }

(apologies for typos, not at my main computer right now, this is just for illustration anyway)

Of course if I were doing a complex property list bridge I would extract some of this out, but if the above is all I need then IMO the union example is more convenient to work with, and produces less pollution of the addProperty signature, while the compiler can still optimise it out into separate functions (by eliminating the switch for each type).

I could alternatively use a protocol to add a .plistValue computed property to all plist compatible types, but again that seems like overkill, and moves the code out of my single function even though that may be all I really need, it also makes it less clear what all of those types are (without building docs to do so). Or I could use an enum, but again, for small use cases that can be a bit overkill, and isn't as convenient in cases that have similar, but different, union types.

Ultimately it's an issue of choice; not every case will be better using or not using union types, they're just a convenience that can benefit some cases. If you find yourself using the same union types a lot then, yes, probably time to think about a protocol or an enum, but that's a big step in simpler cases.

···

On 19 Aug 2016, at 08:42, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

But you can do that already with protocols, can't you?
On Fri, Aug 19, 2016 at 2:24 AM Haravikk via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
I'm a +1 for union types.

My main reason for wanting it is to eliminate (some) function overloads; behind the scenes the compiler may still produce one compiled function per union type (for performance), but at a high level we only need to worry about one implementation, and one call signature, which I think is a good thing.

On 11 Aug 2016, at 02:28, Cao Jiannan via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

Hi all,

I want to make a discussion about union type for swift 4.
See https://github.com/frogcjn/swift-evolution/blob/master/proposals/xxxx-union-type.md

Add union type grammar, represents the type which is one of other types.

var stringOrURL: String | URL = "https://www.apple.com <https://www.apple.com/&gt;&quot;
Now, if we using the new union type feature, we can declare type conveniently, No other type declaration, and compiler will automatically calculate the common interface.

func input(value: A | B | C) {
    print(value.commonProperty) // type checker will calculate the common interface, developer just use it out of box
    switch value {
    case let value as A:
        // value is type A
        print(value.propertyInA)
    case let value as B:
        // value is type B
        print(value.propertyInB)
    case let value as C:
        // value is type C
        print(value.propertyInC)
    }
    // there is no default case other than A, B or C. we already declared that.
}
Note: A, B, C can be either class or protocol, or any other types. This leaves developer more freedom.

Impact on existing code

This is a new feature, developer who need declare common type will alter to this new grammar.
Enum based version optional or IUO will be replaced by Union-based ones. Any optional type will automatically replaced by union type

<https://github.com/frogcjn/swift-evolution/blob/master/proposals/xxxx-union-type.md#detailed-design&gt;\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_\_
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

While I am a big fan of union types, I think an enum based solution for your problem is not too bad. Just use the enums only for the intersection results, i.e.

enum Intersection1d {
    case point(Point)
    case line(Line)
}

Same for Intersection2d, but adding case plane(Plane) and so on for higher dimensions.

This way the purpose of these types is clear which was not the case for a more general GeometryValue.

-Thorsten

···

Am 15.08.2016 um 16:29 schrieb Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org>:

I considered the enum approach but it is very tedious to use "space.add(.point(point))" which also doesn't add any clarity.
The other approach where we add the properties of a Point as associated values also has a major drawback:

// you cannot create a point without "erasing" it's type
let point = GeometryValue.point(x: 4, y: 5)

// if you already have a point instance
"space.add(.point(x: point.x, y: point.y))"

If the union type feature is implemented the intersection of a Point and a Line returns (Point | Line) which is a subtype of a general GeometryValue eg. (Point | Line | Plane) and additionally cannot be checked for a Plane since it doesn't make sense.

The problem with a protocol is that there are only a few methods in common and again in a method like intersect there is no exhaustiveness check in switches.

Best regards
Maximilian

Am 12.08.2016 um 17:16 schrieb Sean Heber <sean@fifthace.com>:

As an aside, you could use an enum instead of a protocol to avoid the problem of not having exhaustive switches:

enum GeometryValue {
case point(Point)
case line(Line)
}

And perhaps (depending on circumstances) you might not even need a Point and Line struct, so you could just put those values inside the enum:

enum GeometryValue {
case point(x: Float, y: Float)
case line(x: Float, y: Float, angle: Float)
}

However personally, I think using a protocol is the more Swifty approach to this problem. If I end up not having any common properties or functions that apply to that one umbrella protocol, then I consider that a signal that I’m modeling my solution incorrectly.

l8r
Sean

On Aug 12, 2016, at 6:24 AM, Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org> wrote:

Hi Cao,

I would be in favor until I find another approach to this problem:

Consider you have a geometry framework and two types: Point and Line

An intersection between two lines can be either none, a point or a line (if both are identical).

The return type would probably be (Point | Line)?

I've modeled it with an empty protocol "GeometryType". However this has a major disadvantage:
If you have a general "GeometryType?" you have to cast it in a switch to the specific type.
In case of (Point| Line)? the switch statement can be checked for exhaustiveness.

For future directions:

There should also be a subtype relationship:

let tu: (T | U) = T()
let tuv: (T | U | V) = tu // works

Overloaded functions/operators could also take Union types based on their overloads:

func take(_ i: Int) -> String { ... }

func take(_ s: String) -> Int? { ... }

let value: (Int | String) = "1234"
let value2 = take(value) // returns (String | Int?)

Best regards
Maximilian

Am 11.08.2016 um 03:28 schrieb Cao Jiannan via swift-evolution <swift-evolution@swift.org>:

Hi all,

I want to make a discussion about union type for swift 4.
See https://github.com/frogcjn/swift-evolution/blob/master/proposals/xxxx-union-type.md

Add union type grammar, represents the type which is one of other types.

var stringOrURL: String | URL = "https://www.apple.com"
Now, if we using the new union type feature, we can declare type conveniently, No other type declaration, and compiler will automatically calculate the common interface.

func input(value: A | B |
C) {

print(value.commonProperty) // type checker will calculate the common interface, developer just use it out of box

switch
value {

case let value as
A:

// value is type A

print(value.
propertyInA)

case let value as
B:

// value is type B

print(value.
propertyInB)

case let value as
C:

// value is type C

print(value.
propertyInC)
  }

// there is no default case other than A, B or C. we already declared that.

}

Note: A, B, C can be either class or protocol, or any other types. This leaves developer more freedom.

Impact on existing code

  • This is a new feature, developer who need declare common type will alter to this new grammar.
  • Enum based version optional or IUO will be replaced by Union-based ones. Any optional type will automatically replaced by union type

_______________________________________________
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

A union in C is a type overlay, not a type selector. The mechanism for what you propose would probably be implemented quite differently (maintaining type safety). As such I believe it should not be called a union as that can put people (with C experience) on the wrong feet. Suggestion: “Implicit Protocols” ?

Regards,
Rien.

···

On 19 Aug 2016, at 13:51, Haravikk via swift-evolution <swift-evolution@swift.org> wrote:

When dealing with your own types, sure, but if you're talking about standard types or types from other libraries then it would mean extending them with whatever it is you actually need, which I'm not sure is the best way to do it. Type unions are much simpler, and keep all of the code within a single function.

To give a simplistic example, consider a method for adding a property to a property list file. I'm going to trim this down considerably, but it'll give an idea hopefully:

  func addProperty(key:String, value:Int | String, file:NSFile) {
    var xml = "<key>\(key)</key>\n"
    switch value {
      case value as Int:
        xml += "<number>\(value)</number>\n"
      case value as String:
        xml += "<string>\(value)</string>\n"
    }

    // Actually store the xml string in the file here
  }

Currently you might instead do this like:

  func addProperty(key:String, value:Int, file:NSFile) {
    addProperty(key: key, rawValue: "<number>\(value)</number>", file: file)
  }
  func addProperty(key:String, value:String, file:NSFile) {
    addProperty(key: key, rawValue: "<string>\(value)</string>", file: file)
  }
  func addProperty(key:String, rawValue:String, file:NSFile) {
    var xml = "<key>\(key)</key>\n\(rawValue\)\n"
    // Actually store the xml string in the file here
  }

(apologies for typos, not at my main computer right now, this is just for illustration anyway)

Of course if I were doing a complex property list bridge I would extract some of this out, but if the above is all I need then IMO the union example is more convenient to work with, and produces less pollution of the addProperty signature, while the compiler can still optimise it out into separate functions (by eliminating the switch for each type).

I could alternatively use a protocol to add a .plistValue computed property to all plist compatible types, but again that seems like overkill, and moves the code out of my single function even though that may be all I really need, it also makes it less clear what all of those types are (without building docs to do so). Or I could use an enum, but again, for small use cases that can be a bit overkill, and isn't as convenient in cases that have similar, but different, union types.

Ultimately it's an issue of choice; not every case will be better using or not using union types, they're just a convenience that can benefit some cases. If you find yourself using the same union types a lot then, yes, probably time to think about a protocol or an enum, but that's a big step in simpler cases.

On 19 Aug 2016, at 08:42, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

But you can do that already with protocols, can't you?
On Fri, Aug 19, 2016 at 2:24 AM Haravikk via swift-evolution <swift-evolution@swift.org> wrote:
I'm a +1 for union types.

My main reason for wanting it is to eliminate (some) function overloads; behind the scenes the compiler may still produce one compiled function per union type (for performance), but at a high level we only need to worry about one implementation, and one call signature, which I think is a good thing.

On 11 Aug 2016, at 02:28, Cao Jiannan via swift-evolution <swift-evolution@swift.org> wrote:

Hi all,

I want to make a discussion about union type for swift 4.
See https://github.com/frogcjn/swift-evolution/blob/master/proposals/xxxx-union-type.md

Add union type grammar, represents the type which is one of other types.

var stringOrURL: String | URL = "https://www.apple.com"
Now, if we using the new union type feature, we can declare type conveniently, No other type declaration, and compiler will automatically calculate the common interface.

func input(value: A | B |
C) {
    
print(value.commonProperty) // type checker will calculate the common interface, developer just use it out of box

switch
value {
    
case let value as
A:
        
// value is type A

print(value.
propertyInA)
    
case let value as
B:
        
// value is type B

print(value.
propertyInB)
    
case let value as
C:
        
// value is type C

print(value.
propertyInC)
    }
    
// there is no default case other than A, B or C. we already declared that.

}

Note: A, B, C can be either class or protocol, or any other types. This leaves developer more freedom.

Impact on existing code

  • This is a new feature, developer who need declare common type will alter to this new grammar.
  • Enum based version optional or IUO will be replaced by Union-based ones. Any optional type will automatically replaced by union type

_______________________________________________
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

When dealing with your own types, sure, but if you're talking about
standard types or types from other libraries then it would mean extending
them with whatever it is you actually need, which I'm not sure is the best
way to do it. Type unions are much simpler, and keep all of the code within
a single function.

To give a simplistic example, consider a method for adding a property to a
property list file. I'm going to trim this down considerably, but it'll
give an idea hopefully:

FWIW, This reminds me a discussion about ad-hoc(anonymous) enums, we had in the list. There were also examples when such ad-hoc enums could be useful within a function/small block of code:

  func scaleAndCropImage(
      image: UIImage,
      toSize size: CGSize,
      *operation: (.fit | .fill) = .fit*
      ) -> UIImage {

var codePath : (.one | .two | .three) = .one
switch codePath {
   case .one : ...
   case .two : ...
   case .three : ...
}

etc..

And AFAIR there was no wide support for this feature.
I don't feel like we need the union type as proposed, but I do would like to see such ad-hoc enums in Swift probably with support of associated type for cases, like here:

func addProperty(key:String, value: (.int(Int) | .string(String) | .text(String)), file:NSFile) {
..
}

so, value will be a standard enum with 3 cases .int/.string/.text, with associated types. IMO this approach is more powerful and useful, *probably* easily to implement than Union types, use already existed abstraction of 'enum' etc.

Opinions?

···

On 19.08.2016 14:51, Haravikk via swift-evolution wrote:

func addProperty(key:String, value:Int | String, file:NSFile) {
var xml = "<key>\(key)</key>\n"
switch value {
case value as Int:
xml += "<number>\(value)</number>\n"
case value as String:
xml += "<string>\(value)</string>\n"
}

// Actually store the xml string in the file here
}

Currently you might instead do this like:

func addProperty(key:String, value:Int, file:NSFile) {
addProperty(key: key, rawValue: "<number>\(value)</number>", file: file)
}
func addProperty(key:String, value:String, file:NSFile) {
addProperty(key: key, rawValue: "<string>\(value)</string>", file: file)
}
func addProperty(key:String, rawValue:String, file:NSFile) {
var xml = "<key>\(key)</key>\n\(rawValue\)\n"
// Actually store the xml string in the file here
}

(apologies for typos, not at my main computer right now, this is just for
illustration anyway)

Of course if I were doing a complex property list bridge I would extract
some of this out, but if the above is all I need then IMO the union example
is more convenient to work with, and produces less pollution of the
addProperty signature, while the compiler can still optimise it out into
separate functions (by eliminating the switch for each type).

I could alternatively use a protocol to add a .plistValue computed property
to all plist compatible types, but again that seems like overkill, and
moves the code out of my single function even though that may be all I
really need, it also makes it less clear what all of those types are
(without building docs to do so). Or I could use an enum, but again, for
small use cases that can be a bit overkill, and isn't as convenient in
cases that have similar, but different, union types.

Ultimately it's an issue of choice; not every case will be better using or
not using union types, they're just a convenience that can benefit some
cases. If you find yourself using the same union types a lot then, yes,
probably time to think about a protocol or an enum, but that's a big step
in simpler cases.

On 19 Aug 2016, at 08:42, Xiaodi Wu <xiaodi.wu@gmail.com >> <mailto:xiaodi.wu@gmail.com>> wrote:

But you can do that already with protocols, can't you?
On Fri, Aug 19, 2016 at 2:24 AM Haravikk via swift-evolution >> <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

    I'm a +1 for union types.

    My main reason for wanting it is to eliminate (some) function
    overloads; behind the scenes the compiler may still produce one
    compiled function per union type (for performance), but at a high
    level we only need to worry about one implementation, and one call
    signature, which I think is a good thing.

    On 11 Aug 2016, at 02:28, Cao Jiannan via swift-evolution >>> <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

    Hi all,

    I want to make a discussion about union type for swift 4.
    See https://github.com/frogcjn/swift-evolution/blob/master/proposals/xxxx-union-type.md

    Add union type grammar, represents the type which is one of other types.

    var stringOrURL: String | URL = "https://www.apple.com <https://www.apple.com/&gt;&quot;

    Now, if we using the new union type feature, we can declare type
    conveniently, No other type declaration, and compiler will
    automatically calculate the common interface.

    func input(value: A | B | C) {
        print(value.commonProperty) // type checker will calculate the common interface, developer just
    use it out of box
        switch value {
        case let value as A:
            // value is type A
            print(value.propertyInA)
        case let value as B:
            // value is type B
            print(value.propertyInB)
        case let value as C:
            // value is type C
            print(value.propertyInC)
        }
        // there is no default case other than A, B or C. we already
    declared that.
    }

    Note: A, B, C can be either class or protocol, or any other types.
    This leaves developer more freedom.

        Impact on existing code

      * This is a new feature, developer who need declare common type
        will alter to this new grammar.
      * Enum based version optional or IUO will be replaced by
        Union-based ones. Any optional type will automatically replaced
        by union type

        ________
        <https://github.com/frogcjn/swift-evolution/blob/master/proposals/xxxx-union-type.md#detailed-design&gt;

    _______________________________________________
    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
https://lists.swift.org/mailman/listinfo/swift-evolution

While purpose of the types are clear in this case there is not only intersection. I also want to find out the distance between different GeometryTypes and other properties like angels between two lines or a Line and a Plane but this doesn't make sense for a Point and some other GeometryType.

Therefore a GeometryType with subtypes is almost the perfect solution. I imagine the perfect solution would be to have something like a "newtype" feature which is similar to a protocol but the types which "conform" to it are known at compile time.

Best regards
Maximilian

···

Am 18.08.2016 um 07:14 schrieb Thorsten Seitz <tseitz42@icloud.com>:

While I am a big fan of union types, I think an enum based solution for your problem is not too bad. Just use the enums only for the intersection results, i.e.

enum Intersection1d {
   case point(Point)
   case line(Line)
}

Same for Intersection2d, but adding case plane(Plane) and so on for higher dimensions.

This way the purpose of these types is clear which was not the case for a more general GeometryValue.

-Thorsten

Am 15.08.2016 um 16:29 schrieb Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org>:

I considered the enum approach but it is very tedious to use "space.add(.point(point))" which also doesn't add any clarity.
The other approach where we add the properties of a Point as associated values also has a major drawback:

// you cannot create a point without "erasing" it's type
let point = GeometryValue.point(x: 4, y: 5)

// if you already have a point instance
"space.add(.point(x: point.x, y: point.y))"

If the union type feature is implemented the intersection of a Point and a Line returns (Point | Line) which is a subtype of a general GeometryValue eg. (Point | Line | Plane) and additionally cannot be checked for a Plane since it doesn't make sense.

The problem with a protocol is that there are only a few methods in common and again in a method like intersect there is no exhaustiveness check in switches.

Best regards
Maximilian

Am 12.08.2016 um 17:16 schrieb Sean Heber <sean@fifthace.com>:

As an aside, you could use an enum instead of a protocol to avoid the problem of not having exhaustive switches:

enum GeometryValue {
case point(Point)
case line(Line)
}

And perhaps (depending on circumstances) you might not even need a Point and Line struct, so you could just put those values inside the enum:

enum GeometryValue {
case point(x: Float, y: Float)
case line(x: Float, y: Float, angle: Float)
}

However personally, I think using a protocol is the more Swifty approach to this problem. If I end up not having any common properties or functions that apply to that one umbrella protocol, then I consider that a signal that I’m modeling my solution incorrectly.

l8r
Sean

On Aug 12, 2016, at 6:24 AM, Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org> wrote:

Hi Cao,

I would be in favor until I find another approach to this problem:

Consider you have a geometry framework and two types: Point and Line

An intersection between two lines can be either none, a point or a line (if both are identical).

The return type would probably be (Point | Line)?

I've modeled it with an empty protocol "GeometryType". However this has a major disadvantage:
If you have a general "GeometryType?" you have to cast it in a switch to the specific type.
In case of (Point| Line)? the switch statement can be checked for exhaustiveness.

For future directions:

There should also be a subtype relationship:

let tu: (T | U) = T()
let tuv: (T | U | V) = tu // works

Overloaded functions/operators could also take Union types based on their overloads:

func take(_ i: Int) -> String { ... }

func take(_ s: String) -> Int? { ... }

let value: (Int | String) = "1234"
let value2 = take(value) // returns (String | Int?)

Best regards
Maximilian

Am 11.08.2016 um 03:28 schrieb Cao Jiannan via swift-evolution <swift-evolution@swift.org>:

Hi all,

I want to make a discussion about union type for swift 4.
See https://github.com/frogcjn/swift-evolution/blob/master/proposals/xxxx-union-type.md

Add union type grammar, represents the type which is one of other types.

var stringOrURL: String | URL = "https://www.apple.com"
Now, if we using the new union type feature, we can declare type conveniently, No other type declaration, and compiler will automatically calculate the common interface.

func input(value: A | B |
C) {

print(value.commonProperty) // type checker will calculate the common interface, developer just use it out of box

switch
value {

case let value as
A:

// value is type A

print(value.
propertyInA)

case let value as
B:

// value is type B

print(value.
propertyInB)

case let value as
C:

// value is type C

print(value.
propertyInC)
}

// there is no default case other than A, B or C. we already declared that.

}

Note: A, B, C can be either class or protocol, or any other types. This leaves developer more freedom.

Impact on existing code

• This is a new feature, developer who need declare common type will alter to this new grammar.
• Enum based version optional or IUO will be replaced by Union-based ones. Any optional type will automatically replaced by union type

_______________________________________________
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

But there are extremely complicated interactions between different pairs of types:

  Line.intersection(with: Line) -> Void | Point | Line
  Line.intersection(with: Circle) -> Void | Point | (Point, Point)
  Line.intersection(with: Polygon) -> [Point | Line]
  
  Circle.intersection(with: Line) -> Void | Point | Line
  Circle.intersection(with: Circle) -> Void | Point | (Point, Point) | Circle
  Circle.intersection(with: Polygon) -> [Point | (Point, Point)]
  
  Polygon.intersection(with: Line) -> [Point | Line]
  Polygon.intersection(with: Circle) -> [Point | (Point, Point)]
  Polygon.intersection(with: Polygon) -> [Point | Line] | Polygon

What exactly are you planning to do with a `[Point | Line] | Polygon`? Honestly, your only real option is to test it for specific subtypes and try to use them. But there's already a type for that kind of thing: an enum. Enums are better for this application because they lend more structure and allow you to explain cases with descriptive labels:

  enum PossiblyCoincidental<CoincidentalType, IntersectingType> {
    case coincidental (CoincidentalType)
    case intersecting (IntersectingType)
  }
  
  typealias LineIntersection = PossiblyCoincidental<Line, Point>

  enum CircleIntersection {
    case tangent (Point)
    case secant (Point, Point)
  }
  
  Line.intersection(with: Line) -> LineIntersection?
  Line.intersection(with: Circle) -> CircleIntersection?
  Line.intersection(with: Polygon) -> [LineIntersection]
  
  Circle.intersection(with: Line) -> CircleIntersection?
  Circle.intersection(with: Circle) -> PossiblyCoincidental<Circle, CircleIntersection>?
  Circle.intersection(with: Polygon) -> [CircleIntersection]
  
  Polygon.intersection(with: Line) -> [LineIntersection]
  Polygon.intersection(with: Circle) -> [CircleIntersection]
  Polygon.intersection(with: Polygon) -> PossiblyCoincidental<Polygon, [LineIntersection]>

This is more complicated, but it's also a lot clearer about what each return type actually means. The existence of rich type information also helps you add functionality:

  protocol IntersectionType {
    var isEmpty: Bool { get }
  }
  
  extension CircleIntersection: IntersectionType {
    var isEmpty: Bool { return false }
  }
  
  extension PossiblyCoincidental: IntersectionType {
    var isEmpty: Bool: { return false }
  }
  
  // Cascade inward if IntersectingType happens to itself be an intersection.
  extension PossiblyCoincidental where IntersectingType: IntersectionType {
    var isEmpty: Bool {
      switch self {
      case .coincidental:
        return false
      case .intersecting(let intersection):
        return intersection.isEmpty
      }
    }
  }
  
  // Note: Using future conditional conformances
  extension Optional: IntersectionType where Wrapped: IntersectionType {
    var isEmpty: Bool {
      return map { $0.isEmpty } ?? true
    }
  }
  
  // Retroactive modeling yay!
  extension Array: IntersectionType where Element: IntersectionType {}

Of course, it might be the case that this is *way* more information than you really need, and you just want to say:

  GeometricElement.intersection(with: GeometricElement) -> [GeometricElement]

But if you want something simple and uniform, you probably don't want the complex `Void | Point | Line`-type stuff, either. Union types are neither simple and uniform, nor complex and descriptive; they are neither fish nor fowl. I just don't see a strong reason to prefer them here.

  * * *

I'll ask again what I think has been the crux of this argument from the beginning, and what hasn't really been satisfactorily answered in several months of discussions.

**What use cases are better served by union types than by the alternatives?**

Can you show us code where union types are clearly better—not just shorter—than an equivalent design based on (depending on the need) protocols, enums, or overloading? What about with minor extensions to these features, like closed protocols (allowing for exhaustive checking of protocol types) or implicit type lifting for enums (allowing you to avoid explicitly constructing the case you need, as Optional.some does)?

There is clearly overlap between union types and some of our other features. When do you expect people would use each feature? Are there other languages which support both union types and sum types (i.e. Swift `enum`s)? When do they use each of these?

(Incidentally, even if we had union types, I don't think we'd want to change Optional's definition. Nested optionals are a bit confusing at times, but they're important for correctness in many cases, like collections of Optionals. If Optional is a union type, you also lose the opportunity to have operations like `map` and `flatMap` on it.)

···

On Aug 18, 2016, at 2:05 AM, Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org> wrote:

While purpose of the types are clear in this case there is not only intersection. I also want to find out the distance between different GeometryTypes and other properties like angels between two lines or a Line and a Plane but this doesn't make sense for a Point and some other GeometryType.

--
Brent Royal-Gordon
Architechies

While purpose of the types are clear in this case there is not only intersection. I also want to find out the distance between different GeometryTypes and other properties like angels between two lines or a Line and a Plane but this doesn't make sense for a Point and some other GeometryType.

But there are extremely complicated interactions between different pairs of types:

  Line.intersection(with: Line) -> Void | Point | Line
  Line.intersection(with: Circle) -> Void | Point | (Point, Point)
  Line.intersection(with: Polygon) -> [Point | Line]
  
  Circle.intersection(with: Line) -> Void | Point | Line
  Circle.intersection(with: Circle) -> Void | Point | (Point, Point) | Circle
  Circle.intersection(with: Polygon) -> [Point | (Point, Point)]
  
  Polygon.intersection(with: Line) -> [Point | Line]
  Polygon.intersection(with: Circle) -> [Point | (Point, Point)]
  Polygon.intersection(with: Polygon) -> [Point | Line] | Polygon

What exactly are you planning to do with a `[Point | Line] | Polygon`? Honestly, your only real option is to test it for specific subtypes and try to use them. But there's already a type for that kind of thing: an enum. Enums are better for this application because they lend more structure and allow you to explain cases with descriptive labels:

  enum PossiblyCoincidental<CoincidentalType, IntersectingType> {
    case coincidental (CoincidentalType)
    case intersecting (IntersectingType)
  }
  
  typealias LineIntersection = PossiblyCoincidental<Line, Point>

  enum CircleIntersection {
    case tangent (Point)
    case secant (Point, Point)
  }
  
  Line.intersection(with: Line) -> LineIntersection?
  Line.intersection(with: Circle) -> CircleIntersection?
  Line.intersection(with: Polygon) -> [LineIntersection]
  
  Circle.intersection(with: Line) -> CircleIntersection?
  Circle.intersection(with: Circle) -> PossiblyCoincidental<Circle, CircleIntersection>?
  Circle.intersection(with: Polygon) -> [CircleIntersection]
  
  Polygon.intersection(with: Line) -> [LineIntersection]
  Polygon.intersection(with: Circle) -> [CircleIntersection]
  Polygon.intersection(with: Polygon) -> PossiblyCoincidental<Polygon, [LineIntersection]>

This is more complicated, but it's also a lot clearer about what each return type actually means. The existence of rich type information also helps you add functionality:

  protocol IntersectionType {
    var isEmpty: Bool { get }
  }
  
  extension CircleIntersection: IntersectionType {
    var isEmpty: Bool { return false }
  }
  
  extension PossiblyCoincidental: IntersectionType {
    var isEmpty: Bool: { return false }
  }
  
  // Cascade inward if IntersectingType happens to itself be an intersection.
  extension PossiblyCoincidental where IntersectingType: IntersectionType {
    var isEmpty: Bool {
      switch self {
      case .coincidental:
        return false
      case .intersecting(let intersection):
        return intersection.isEmpty
      }
    }
  }
  
  // Note: Using future conditional conformances
  extension Optional: IntersectionType where Wrapped: IntersectionType {
    var isEmpty: Bool {
      return map { $0.isEmpty } ?? true
    }
  }
  
  // Retroactive modeling yay!
  extension Array: IntersectionType where Element: IntersectionType {}

Of course, it might be the case that this is *way* more information than you really need, and you just want to say:

  GeometricElement.intersection(with: GeometricElement) -> [GeometricElement]

But if you want something simple and uniform, you probably don't want the complex `Void | Point | Line`-type stuff, either. Union types are neither simple and uniform, nor complex and descriptive; they are neither fish nor fowl. I just don't see a strong reason to prefer them here.

  * * *

I'll ask again what I think has been the crux of this argument from the beginning, and what hasn't really been satisfactorily answered in several months of discussions.

**What use cases are better served by union types than by the alternatives?**

Can you show us code where union types are clearly better—not just shorter—than an equivalent design based on (depending on the need) protocols, enums, or overloading? What about with minor extensions to these features, like closed protocols (allowing for exhaustive checking of protocol types) or implicit type lifting for enums (allowing you to avoid explicitly constructing the case you need, as Optional.some does)?

I think implicit lifting with some additional syntactic sugar would be a very good solution here.

First, an example of an enum with implicit lifting (using the strawman `autolift` modifier which conservatively could be restricted to cases with a single associated value and could not be used on cases with associated values whose type is also the type of an associated value in another case):

enum Foo {
    autolift case .string(String)
    autolift case .int(Int)
    // etc
}

func bar(_ foos: [Foo]) {}

// “a string” is implicitly lifted to be .string(“a string”) and 42 is implicitly lifted to be .int(42)
bar([“a string”, 42])

This would solve the use case I have run into where I was working on a library design and wanted to accept a heterogeneous collection without requiring callers to have any knowledge beyond what types can be uses (i.e. I don’t want them to have to deal with the syntactic noise of manual lifting, but more importantly I would consider an enum like this an implementation detail, not something users should really rely on).

If we *are* going to support implicit lifting like this, why not also adopt syntactic sugar for creating / referencing them in an ad-hoc manner? Rather than writing out the enum above, we could just declare:

func bar(_ foos: [String | Int]) {}

The enum would be created “on demand” each time a distinct set of types was referenced, with implicit lifting for each case. What is the downside of this syntactic sugar? It would bring the “lightweight” feel of union types to Swift while retaining the semantics of enums. This feels like a nice middle ground to me.

As you mention, closed protocols are another possible solution to the design problem I was working on, although it is somewhat less elegant as it would add conformances to the underlying types which is not necessarily desirable.

···

On Aug 19, 2016, at 1:20 AM, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org> wrote:

On Aug 18, 2016, at 2:05 AM, Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org> wrote:

There is clearly overlap between union types and some of our other features. When do you expect people would use each feature? Are there other languages which support both union types and sum types (i.e. Swift `enum`s)? When do they use each of these?

(Incidentally, even if we had union types, I don't think we'd want to change Optional's definition. Nested optionals are a bit confusing at times, but they're important for correctness in many cases, like collections of Optionals. If Optional is a union type, you also lose the opportunity to have operations like `map` and `flatMap` on it.)

--
Brent Royal-Gordon
Architechies

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

Ad-hoc enums have been discussed already, at length, and all the weaknesses
touched on then still apply now. For instance, since they're ad-hoc, can
you pass an instance of type "Int | String" as an argument if the function
expects a "String | Int | Float"? Enums don't have duck typing behavior
like that; if your ad-hoc type does, then it's not very much like an enum;
if it doesn't, it won't feel much like a union type.

Moreover, an ad-hoc "String | Int" may look like a union type, but until
switching over an instance to cast it, you can't invoke any methods common
to String and Int. So it really doesn't feel like a union type at all.

Don't get me wrong--like Brent, I'm not convinced I see a scenario in which
union types would help write clearly better code, just code that is more
"convenient" in the eyes of the beholder. But ad-hoc enums have had their
day on this list, and I'm not sure that re-visiting that discussion is
going to be very fruitful.

In any case, this all seems very, very out of scope for Swift 4.

···

On Fri, Aug 19, 2016 at 9:07 AM Matthew Johnson via swift-evolution < swift-evolution@swift.org> wrote:

> On Aug 19, 2016, at 1:20 AM, Brent Royal-Gordon via swift-evolution < > swift-evolution@swift.org> wrote:
>
>> On Aug 18, 2016, at 2:05 AM, Maximilian Hünenberger via swift-evolution > <swift-evolution@swift.org> wrote:
>>
>> While purpose of the types are clear in this case there is not only
intersection. I also want to find out the distance between different
GeometryTypes and other properties like angels between two lines or a Line
and a Plane but this doesn't make sense for a Point and some other
GeometryType.
>
> But there are extremely complicated interactions between different pairs
of types:
>
> Line.intersection(with: Line) -> Void | Point | Line
> Line.intersection(with: Circle) -> Void | Point | (Point, Point)
> Line.intersection(with: Polygon) -> [Point | Line]
>
> Circle.intersection(with: Line) -> Void | Point | Line
> Circle.intersection(with: Circle) -> Void | Point | (Point, Point)
> Circle
> Circle.intersection(with: Polygon) -> [Point | (Point, Point)]
>
> Polygon.intersection(with: Line) -> [Point | Line]
> Polygon.intersection(with: Circle) -> [Point | (Point, Point)]
> Polygon.intersection(with: Polygon) -> [Point | Line] | Polygon
>
> What exactly are you planning to do with a `[Point | Line] | Polygon`?
Honestly, your only real option is to test it for specific subtypes and try
to use them. But there's already a type for that kind of thing: an enum.
Enums are better for this application because they lend more structure and
allow you to explain cases with descriptive labels:
>
> enum PossiblyCoincidental<CoincidentalType, IntersectingType> {
> case coincidental (CoincidentalType)
> case intersecting (IntersectingType)
> }
>
> typealias LineIntersection = PossiblyCoincidental<Line, Point>
>
> enum CircleIntersection {
> case tangent (Point)
> case secant (Point, Point)
> }
>
> Line.intersection(with: Line) -> LineIntersection?
> Line.intersection(with: Circle) -> CircleIntersection?
> Line.intersection(with: Polygon) -> [LineIntersection]
>
> Circle.intersection(with: Line) -> CircleIntersection?
> Circle.intersection(with: Circle) -> PossiblyCoincidental<Circle,
>?
> Circle.intersection(with: Polygon) -> [CircleIntersection]
>
> Polygon.intersection(with: Line) -> [LineIntersection]
> Polygon.intersection(with: Circle) -> [CircleIntersection]
> Polygon.intersection(with: Polygon) ->
PossiblyCoincidental<Polygon, [LineIntersection]>
>
> This is more complicated, but it's also a lot clearer about what each
return type actually means. The existence of rich type information also
helps you add functionality:
>
> protocol IntersectionType {
> var isEmpty: Bool { get }
> }
>
> extension CircleIntersection: IntersectionType {
> var isEmpty: Bool { return false }
> }
>
> extension PossiblyCoincidental: IntersectionType {
> var isEmpty: Bool: { return false }
> }
>
> // Cascade inward if IntersectingType happens to itself be an
intersection.
> extension PossiblyCoincidental where IntersectingType:
IntersectionType {
> var isEmpty: Bool {
> switch self {
> case .coincidental:
> return false
> case .intersecting(let intersection):
> return intersection.isEmpty
> }
> }
> }
>
> // Note: Using future conditional conformances
> extension Optional: IntersectionType where Wrapped:
IntersectionType {
> var isEmpty: Bool {
> return map { $0.isEmpty } ?? true
> }
> }
>
> // Retroactive modeling yay!
> extension Array: IntersectionType where Element: IntersectionType
{}
>
> Of course, it might be the case that this is *way* more information than
you really need, and you just want to say:
>
> GeometricElement.intersection(with: GeometricElement) ->
[GeometricElement]
>
> But if you want something simple and uniform, you probably don't want
the complex `Void | Point | Line`-type stuff, either. Union types are
neither simple and uniform, nor complex and descriptive; they are neither
fish nor fowl. I just don't see a strong reason to prefer them here.
>
> * * *
>
> I'll ask again what I think has been the crux of this argument from the
beginning, and what hasn't really been satisfactorily answered in several
months of discussions.
>
> **What use cases are better served by union types than by the
alternatives?**
>
> Can you show us code where union types are clearly better—not just
shorter—than an equivalent design based on (depending on the need)
protocols, enums, or overloading? What about with minor extensions to these
features, like closed protocols (allowing for exhaustive checking of
protocol types) or implicit type lifting for enums (allowing you to avoid
explicitly constructing the case you need, as Optional.some does)?

I think implicit lifting with some additional syntactic sugar would be a
very good solution here.

First, an example of an enum with implicit lifting (using the strawman
`autolift` modifier which conservatively could be restricted to cases with
a single associated value and could not be used on cases with associated
values whose type is also the type of an associated value in another case):

enum Foo {
    autolift case .string(String)
    autolift case .int(Int)
    // etc
}

func bar(_ foos: [Foo]) {}

// “a string” is implicitly lifted to be .string(“a string”) and 42 is
implicitly lifted to be .int(42)
bar([“a string”, 42])

This would solve the use case I have run into where I was working on a
library design and wanted to accept a heterogeneous collection without
requiring callers to have any knowledge beyond what types can be uses (i.e.
I don’t want them to have to deal with the syntactic noise of manual
lifting, but more importantly I would consider an enum like this an
implementation detail, not something users should really rely on).

If we *are* going to support implicit lifting like this, why not also
adopt syntactic sugar for creating / referencing them in an ad-hoc manner?
Rather than writing out the enum above, we could just declare:

func bar(_ foos: [String | Int]) {}

The enum would be created “on demand” each time a distinct set of types
was referenced, with implicit lifting for each case. What is the downside
of this syntactic sugar? It would bring the “lightweight” feel of union
types to Swift while retaining the semantics of enums. This feels like a
nice middle ground to me.

As you mention, closed protocols are another possible solution to the
design problem I was working on, although it is somewhat less elegant as it
would add conformances to the underlying types which is not necessarily
desirable.

>
> There is clearly overlap between union types and some of our other
features. When do you expect people would use each feature? Are there other
languages which support both union types and sum types (i.e. Swift
`enum`s)? When do they use each of these?
>
> (Incidentally, even if we had union types, I don't think we'd want to
change Optional's definition. Nested optionals are a bit confusing at
times, but they're important for correctness in many cases, like
collections of Optionals. If Optional is a union type, you also lose the
opportunity to have operations like `map` and `flatMap` on it.)
>
> --
> Brent Royal-Gordon
> Architechies
>
> _______________________________________________
> 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

Ad-hoc enums have been discussed already, at length, and all the weaknesses touched on then still apply now. For instance, since they're ad-hoc, can you pass an instance of type "Int | String" as an argument if the function expects a "String | Int | Float"? Enums don't have duck typing behavior like that; if your ad-hoc type does, then it's not very much like an enum; if it doesn't, it won't feel much like a union type.

IMO one of the nice things about modeling this as syntactic sugar over the semantics of enums is that the answer to this is clear. Right now, no. In the future we may have value type subtyping, in which case the answer may well be yes.

Moreover, an ad-hoc "String | Int" may look like a union type, but until switching over an instance to cast it, you can't invoke any methods common to String and Int. So it really doesn't feel like a union type at all.

I am less convinced of the value of duck-typing, “protocol-like” unions that allow you to access common members than I am of the value of having lightweight syntax for enums that are really just wrappers around a set of possible types.

For the sake of discussion, lets say we adopt the “syntactic sugar over enums” approach here. In cases where accessing common members is really important it will still be possible.

// Remember, `Foo | Bar` is syntactic sugar for a type like `enum FooBar`, but the actual name is anonymous.
extension Foo | Bar {
    func commonMember() {
        switch self {
        case let foo as Foo: foo.commonMember()
        case let bar as Bar: bar.commonMember()
        }
    }
}

Granted, this is boilerplate. But that is maybe a good thing. It will guide people away from abusing this as a duck-typed alternative to real protocols. When you need access to the common members you probably *should* be using a protocol instead.

Don't get me wrong--like Brent, I'm not convinced I see a scenario in which union types would help write clearly better code, just code that is more "convenient" in the eyes of the beholder. But ad-hoc enums have had their day on this list, and I'm not sure that re-visiting that discussion is going to be very fruitful.

There have been several discussions around ad-hoc enums and union types. I don’t recall seeing any of them specifically focused on lightweight syntax layered over enums. I *think* this approach avoids many of the reasons the core team has been opposed unions by not exposing any members directly - all you can do with these “unions” is pattern match to extract the payload.

I would like to see a discussion of something along these lines happen down the road when the time is right.

As I noted in reply to Brent, adding the ability to implicitly lift cases would be sufficient for the use cases I am aware of right now. I would be satisfied if we made that change and no other. But it would always feel like something that is crying out for a bit more syntactic sugar (coming up with names for some of these enums would be awkward - `Foo | Bar` is really what we want and `enum FooOrBar` just obscures the intent).

In any case, this all seems very, very out of scope for Swift 4.

I agree with this, certainly for phase 1 in any case. I don’t want to push a distracting discussion right now. I just wanted to respond to Brent with my thoughts.

···

On Aug 19, 2016, at 9:38 AM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Fri, Aug 19, 2016 at 9:07 AM Matthew Johnson via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:

> On Aug 19, 2016, at 1:20 AM, Brent Royal-Gordon via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>
>> On Aug 18, 2016, at 2:05 AM, Maximilian Hünenberger via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>>
>> While purpose of the types are clear in this case there is not only intersection. I also want to find out the distance between different GeometryTypes and other properties like angels between two lines or a Line and a Plane but this doesn't make sense for a Point and some other GeometryType.
>
> But there are extremely complicated interactions between different pairs of types:
>
> Line.intersection(with: Line) -> Void | Point | Line
> Line.intersection(with: Circle) -> Void | Point | (Point, Point)
> Line.intersection(with: Polygon) -> [Point | Line]
>
> Circle.intersection(with: Line) -> Void | Point | Line
> Circle.intersection(with: Circle) -> Void | Point | (Point, Point) | Circle
> Circle.intersection(with: Polygon) -> [Point | (Point, Point)]
>
> Polygon.intersection(with: Line) -> [Point | Line]
> Polygon.intersection(with: Circle) -> [Point | (Point, Point)]
> Polygon.intersection(with: Polygon) -> [Point | Line] | Polygon
>
> What exactly are you planning to do with a `[Point | Line] | Polygon`? Honestly, your only real option is to test it for specific subtypes and try to use them. But there's already a type for that kind of thing: an enum. Enums are better for this application because they lend more structure and allow you to explain cases with descriptive labels:
>
> enum PossiblyCoincidental<CoincidentalType, IntersectingType> {
> case coincidental (CoincidentalType)
> case intersecting (IntersectingType)
> }
>
> typealias LineIntersection = PossiblyCoincidental<Line, Point>
>
> enum CircleIntersection {
> case tangent (Point)
> case secant (Point, Point)
> }
>
> Line.intersection(with: Line) -> LineIntersection?
> Line.intersection(with: Circle) -> CircleIntersection?
> Line.intersection(with: Polygon) -> [LineIntersection]
>
> Circle.intersection(with: Line) -> CircleIntersection?
> Circle.intersection(with: Circle) -> PossiblyCoincidental<Circle, CircleIntersection>?
> Circle.intersection(with: Polygon) -> [CircleIntersection]
>
> Polygon.intersection(with: Line) -> [LineIntersection]
> Polygon.intersection(with: Circle) -> [CircleIntersection]
> Polygon.intersection(with: Polygon) -> PossiblyCoincidental<Polygon, [LineIntersection]>
>
> This is more complicated, but it's also a lot clearer about what each return type actually means. The existence of rich type information also helps you add functionality:
>
> protocol IntersectionType {
> var isEmpty: Bool { get }
> }
>
> extension CircleIntersection: IntersectionType {
> var isEmpty: Bool { return false }
> }
>
> extension PossiblyCoincidental: IntersectionType {
> var isEmpty: Bool: { return false }
> }
>
> // Cascade inward if IntersectingType happens to itself be an intersection.
> extension PossiblyCoincidental where IntersectingType: IntersectionType {
> var isEmpty: Bool {
> switch self {
> case .coincidental:
> return false
> case .intersecting(let intersection):
> return intersection.isEmpty
> }
> }
> }
>
> // Note: Using future conditional conformances
> extension Optional: IntersectionType where Wrapped: IntersectionType {
> var isEmpty: Bool {
> return map { $0.isEmpty } ?? true
> }
> }
>
> // Retroactive modeling yay!
> extension Array: IntersectionType where Element: IntersectionType {}
>
> Of course, it might be the case that this is *way* more information than you really need, and you just want to say:
>
> GeometricElement.intersection(with: GeometricElement) -> [GeometricElement]
>
> But if you want something simple and uniform, you probably don't want the complex `Void | Point | Line`-type stuff, either. Union types are neither simple and uniform, nor complex and descriptive; they are neither fish nor fowl. I just don't see a strong reason to prefer them here.
>
> * * *
>
> I'll ask again what I think has been the crux of this argument from the beginning, and what hasn't really been satisfactorily answered in several months of discussions.
>
> **What use cases are better served by union types than by the alternatives?**
>
> Can you show us code where union types are clearly better—not just shorter—than an equivalent design based on (depending on the need) protocols, enums, or overloading? What about with minor extensions to these features, like closed protocols (allowing for exhaustive checking of protocol types) or implicit type lifting for enums (allowing you to avoid explicitly constructing the case you need, as Optional.some does)?

I think implicit lifting with some additional syntactic sugar would be a very good solution here.

First, an example of an enum with implicit lifting (using the strawman `autolift` modifier which conservatively could be restricted to cases with a single associated value and could not be used on cases with associated values whose type is also the type of an associated value in another case):

enum Foo {
    autolift case .string(String)
    autolift case .int(Int)
    // etc
}

func bar(_ foos: [Foo]) {}

// “a string” is implicitly lifted to be .string(“a string”) and 42 is implicitly lifted to be .int(42)
bar([“a string”, 42])

This would solve the use case I have run into where I was working on a library design and wanted to accept a heterogeneous collection without requiring callers to have any knowledge beyond what types can be uses (i.e. I don’t want them to have to deal with the syntactic noise of manual lifting, but more importantly I would consider an enum like this an implementation detail, not something users should really rely on).

If we *are* going to support implicit lifting like this, why not also adopt syntactic sugar for creating / referencing them in an ad-hoc manner? Rather than writing out the enum above, we could just declare:

func bar(_ foos: [String | Int]) {}

The enum would be created “on demand” each time a distinct set of types was referenced, with implicit lifting for each case. What is the downside of this syntactic sugar? It would bring the “lightweight” feel of union types to Swift while retaining the semantics of enums. This feels like a nice middle ground to me.

As you mention, closed protocols are another possible solution to the design problem I was working on, although it is somewhat less elegant as it would add conformances to the underlying types which is not necessarily desirable.

>
> There is clearly overlap between union types and some of our other features. When do you expect people would use each feature? Are there other languages which support both union types and sum types (i.e. Swift `enum`s)? When do they use each of these?
>
> (Incidentally, even if we had union types, I don't think we'd want to change Optional's definition. Nested optionals are a bit confusing at times, but they're important for correctness in many cases, like collections of Optionals. If Optional is a union type, you also lose the opportunity to have operations like `map` and `flatMap` on it.)
>
> --
> Brent Royal-Gordon
> Architechies
>
> _______________________________________________
> 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

Ad-hoc enums have been discussed already, at length, and all the weaknesses touched on then still apply now. For instance, since they're ad-hoc, can you pass an instance of type "Int | String" as an argument if the function expects a "String | Int | Float"? Enums don't have duck typing behavior like that; if your ad-hoc type does, then it's not very much like an enum; if it doesn't, it won't feel much like a union type.

While ad-hoc enums are certainly similar I don't think that this problem applies to unions; the problem with ad-hoc enums is that while cases may have the same name, the meaning of a case may not be identical, so compatibility is uncertain. For type unions I'd say this isn't an issue; I'd say that yes, String | Int is compatible with String | Int | Float as every possible value can be carried over (whereas the reverse is not true), they're just values of one of several types, so as long as the conversion is possible, it should be fine to pass it on (or rather, repackage it behind the scenes).

Moreover, an ad-hoc "String | Int" may look like a union type, but until switching over an instance to cast it, you can't invoke any methods common to String and Int. So it really doesn't feel like a union type at all.

Could it not do that though? I'd say that a union type should conform to any common protocols that its members conform to; if this can be done in the initial release then great, otherwise it can come later.

···

On 19 Aug 2016, at 15:38, Xiaodi Wu via swift-evolution <swift-evolution@swift.org> wrote:

This exact scenario came up in the context of iterating over the members of a heterogeneous tuple. If I wasn't on my phone, I'd find a link to the relevant posts, but IIRC the gist of it is (my wording) "it doesn't work that way", or at least not without a *lot* of work and a high load on the type-checker.

- Dave Sweeris

···

On Aug 20, 2016, at 10:36, Haravikk via swift-evolution <swift-evolution@swift.org> wrote:

On 19 Aug 2016, at 15:38, Xiaodi Wu via swift-evolution <swift-evolution@swift.org> wrote:

Ad-hoc enums have been discussed already, at length, and all the weaknesses touched on then still apply now. For instance, since they're ad-hoc, can you pass an instance of type "Int | String" as an argument if the function expects a "String | Int | Float"? Enums don't have duck typing behavior like that; if your ad-hoc type does, then it's not very much like an enum; if it doesn't, it won't feel much like a union type.

While ad-hoc enums are certainly similar I don't think that this problem applies to unions; the problem with ad-hoc enums is that while cases may have the same name, the meaning of a case may not be identical, so compatibility is uncertain. For type unions I'd say this isn't an issue; I'd say that yes, String | Int is compatible with String | Int | Float as every possible value can be carried over (whereas the reverse is not true), they're just values of one of several types, so as long as the conversion is possible, it should be fine to pass it on (or rather, repackage it behind the scenes).

Moreover, an ad-hoc "String | Int" may look like a union type, but until switching over an instance to cast it, you can't invoke any methods common to String and Int. So it really doesn't feel like a union type at all.

Could it not do that though? I'd say that a union type should conform to any common protocols that its members conform to; if this can be done in the initial release then great, otherwise it can come later.

Conforming to common protocols would be much better than an implicit ad-hoc / duck-typed protocol that simply exposes all common members. But there is strong opposition to unions, much of which is related to implementation complexity. It seems to me that the path to having unions or a union-ish feature receiving serious consideration is to demonstrate the value they can offer even with relatively restricted functionality (such as syntactic sugar for enums with implicit lifting). If that is successful we will have an opportunity to work with them and make a case for enhancements in the future.

Also, it won't always possible for a union to conform to a protocol conformed to by all member types. If the protocol has `Self` requirements in argument position it would not be able to conform and if it has associated type requirements which are bound to different concrete types in the types making up the union it would also not be able to conform.

Matthew

···

On Aug 20, 2016, at 10:36 AM, Haravikk <swift-evolution@haravikk.me> wrote:

On 19 Aug 2016, at 15:38, Xiaodi Wu via swift-evolution <swift-evolution@swift.org> wrote:

Ad-hoc enums have been discussed already, at length, and all the weaknesses touched on then still apply now. For instance, since they're ad-hoc, can you pass an instance of type "Int | String" as an argument if the function expects a "String | Int | Float"? Enums don't have duck typing behavior like that; if your ad-hoc type does, then it's not very much like an enum; if it doesn't, it won't feel much like a union type.

While ad-hoc enums are certainly similar I don't think that this problem applies to unions; the problem with ad-hoc enums is that while cases may have the same name, the meaning of a case may not be identical, so compatibility is uncertain. For type unions I'd say this isn't an issue; I'd say that yes, String | Int is compatible with String | Int | Float as every possible value can be carried over (whereas the reverse is not true), they're just values of one of several types, so as long as the conversion is possible, it should be fine to pass it on (or rather, repackage it behind the scenes).

Moreover, an ad-hoc "String | Int" may look like a union type, but until switching over an instance to cast it, you can't invoke any methods common to String and Int. So it really doesn't feel like a union type at all.

Could it not do that though? I'd say that a union type should conform to any common protocols that its members conform to; if this can be done in the initial release then great, otherwise it can come later.

Maybe I'm not getting something. But if you only want T | U | V to expose
members required by common protocols P, Q, and R, since you know the types
at compile time, you also know the common protocols. Why wouldn't you just
write P & Q & R, and if necessary precondition(x is T || x is U || x is V)?

···

On Sat, Aug 20, 2016 at 12:35 Matthew Johnson <matthew@anandabits.com> wrote:

> On Aug 20, 2016, at 10:36 AM, Haravikk <swift-evolution@haravikk.me> > wrote:
>
>
>> On 19 Aug 2016, at 15:38, Xiaodi Wu via swift-evolution < > swift-evolution@swift.org> wrote:
>>
>> Ad-hoc enums have been discussed already, at length, and all the
weaknesses touched on then still apply now. For instance, since they're
ad-hoc, can you pass an instance of type "Int | String" as an argument if
the function expects a "String | Int | Float"? Enums don't have duck typing
behavior like that; if your ad-hoc type does, then it's not very much like
an enum; if it doesn't, it won't feel much like a union type.
>
> While ad-hoc enums are certainly similar I don't think that this problem
applies to unions; the problem with ad-hoc enums is that while cases may
have the same name, the meaning of a case may not be identical, so
compatibility is uncertain. For type unions I'd say this isn't an issue;
I'd say that yes, String | Int is compatible with String | Int | Float as
every possible value can be carried over (whereas the reverse is not true),
they're just values of one of several types, so as long as the conversion
is possible, it should be fine to pass it on (or rather, repackage it
behind the scenes).
>
>> Moreover, an ad-hoc "String | Int" may look like a union type, but
until switching over an instance to cast it, you can't invoke any methods
common to String and Int. So it really doesn't feel like a union type at
all.
>
> Could it not do that though? I'd say that a union type should conform to
any common protocols that its members conform to; if this can be done in
the initial release then great, otherwise it can come later.

Conforming to common protocols would be much better than an implicit
ad-hoc / duck-typed protocol that simply exposes all common members. But
there is strong opposition to unions, much of which is related to
implementation complexity. It seems to me that the path to having unions
or a union-ish feature receiving serious consideration is to demonstrate
the value they can offer even with relatively restricted functionality
(such as syntactic sugar for enums with implicit lifting). If that is
successful we will have an opportunity to work with them and make a case
for enhancements in the future.

Also, it won't always possible for a union to conform to a protocol
conformed to by all member types. If the protocol has `Self` requirements
in argument position it would not be able to conform and if it has
associated type requirements which are bound to different concrete types in
the types making up the union it would also not be able to conform.

Matthew

Maybe I'm not getting something. But if you only want T | U | V to expose members required by common protocols P, Q, and R, since you know the types at compile time, you also know the common protocols. Why wouldn't you just write P & Q & R, and if necessary precondition(x is T || x is U || x is V)?

This isn’t something I personally was advocating for (in fact I specifically suggested *not* proposing this would be a better strategy for receiving serious consideration of something union-ish).

However, if we *did* do something like this it would provide a static verification that the precondition is met while still exposing the members of the common protocol(s) directly. That is substantially better than runtime verification of the precondition or the boilerplate necessary to expose provide forwarding members.

My personal interest in something union-ish is really along the lines of syntactic sugar for enums.

Rather than writing this:

enum StringOrIntOrBool {
    case .string(String)
    case .int(Int)
    case .bool(Bool)
}

func foo(values: [StringOrIntOrBool]) {}

foo([.string(“hello”), .int(42)])

I would prefer to just write:

func foo(values: String | Int | Bool) {}

foo([“hello”, 42])

The use case for this I found is in designing DSLs of various kinds where you need to accept a heterogenous collection of a specific fixed list of concrete types. The primary concern is to have static verification of the possible values provided by the caller without requiring callers to manually instantiate cases and without polluting the member types with unwanted conformance to a single-use protocol (which would be an alternative if we get sealed protocols).

If we add automatic lifting as Brent suggested that solves the primary concern I have. If we do add that, introducing the union-like notation would make sense as convenient syntactic sugar for use in these scenarios where the sugar states the intent more clearly and concisely than something a contrived name like `StringOrIntOrBool`. But as I have already stated, that is as secondary concern and automatic lifting alone would solve the primary (call site / library user) concern.

Matthew

···

On Aug 20, 2016, at 12:27 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Sat, Aug 20, 2016 at 12:35 Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:

> On Aug 20, 2016, at 10:36 AM, Haravikk <swift-evolution@haravikk.me <mailto:swift-evolution@haravikk.me>> wrote:
>
>
>> On 19 Aug 2016, at 15:38, Xiaodi Wu via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>>
>> Ad-hoc enums have been discussed already, at length, and all the weaknesses touched on then still apply now. For instance, since they're ad-hoc, can you pass an instance of type "Int | String" as an argument if the function expects a "String | Int | Float"? Enums don't have duck typing behavior like that; if your ad-hoc type does, then it's not very much like an enum; if it doesn't, it won't feel much like a union type.
>
> While ad-hoc enums are certainly similar I don't think that this problem applies to unions; the problem with ad-hoc enums is that while cases may have the same name, the meaning of a case may not be identical, so compatibility is uncertain. For type unions I'd say this isn't an issue; I'd say that yes, String | Int is compatible with String | Int | Float as every possible value can be carried over (whereas the reverse is not true), they're just values of one of several types, so as long as the conversion is possible, it should be fine to pass it on (or rather, repackage it behind the scenes).
>
>> Moreover, an ad-hoc "String | Int" may look like a union type, but until switching over an instance to cast it, you can't invoke any methods common to String and Int. So it really doesn't feel like a union type at all.
>
> Could it not do that though? I'd say that a union type should conform to any common protocols that its members conform to; if this can be done in the initial release then great, otherwise it can come later.

Conforming to common protocols would be much better than an implicit ad-hoc / duck-typed protocol that simply exposes all common members. But there is strong opposition to unions, much of which is related to implementation complexity. It seems to me that the path to having unions or a union-ish feature receiving serious consideration is to demonstrate the value they can offer even with relatively restricted functionality (such as syntactic sugar for enums with implicit lifting). If that is successful we will have an opportunity to work with them and make a case for enhancements in the future.

Also, it won't always possible for a union to conform to a protocol conformed to by all member types. If the protocol has `Self` requirements in argument position it would not be able to conform and if it has associated type requirements which are bound to different concrete types in the types making up the union it would also not be able to conform.

Matthew

That's good information. I guess a partly implied question which I'm not
entirely sure of would be:

What are the scenarios you've encountered where it actually matters that an
algorithm that works with any P & Q & R must ensure that the argument is
one of a fixed list of concrete types?

Examples of String, Int, Bool call to mind JSON parsing, but in that
scenario I'd expect the fixed list of concrete types would be a constraint
of the parsing logic long before you get to any algorithm that operates on
a P & Q & R.

···

On Sat, Aug 20, 2016 at 14:09 Matthew Johnson <matthew@anandabits.com> wrote:

On Aug 20, 2016, at 12:27 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

Maybe I'm not getting something. But if you only want T | U | V to expose
members required by common protocols P, Q, and R, since you know the types
at compile time, you also know the common protocols. Why wouldn't you just
write P & Q & R, and if necessary precondition(x is T || x is U || x is V)?

This isn’t something I personally was advocating for (in fact I
specifically suggested *not* proposing this would be a better strategy for
receiving serious consideration of something union-ish).

However, if we *did* do something like this it would provide a static
verification that the precondition is met while still exposing the members
of the common protocol(s) directly. That is substantially better than
runtime verification of the precondition or the boilerplate necessary to
expose provide forwarding members.

My personal interest in something union-ish is really along the lines of
syntactic sugar for enums.

Rather than writing this:

enum StringOrIntOrBool {
    case .string(String)
    case .int(Int)
    case .bool(Bool)
}

func foo(values: [StringOrIntOrBool]) {}

foo([.string(“hello”), .int(42)])

I would prefer to just write:

func foo(values: String | Int | Bool) {}

foo([“hello”, 42])

The use case for this I found is in designing DSLs of various kinds where
you need to accept a heterogenous collection of a specific fixed list of
concrete types. The primary concern is to have static verification of the
possible values provided by the caller without requiring callers to
manually instantiate cases and without polluting the member types with
unwanted conformance to a single-use protocol (which would be an
alternative if we get sealed protocols).

If we add automatic lifting as Brent suggested that solves the primary
concern I have. If we do add that, introducing the union-like notation
would make sense as convenient syntactic sugar for use in these scenarios
where the sugar states the intent more clearly and concisely than something
a contrived name like `StringOrIntOrBool`. But as I have already stated,
that is as secondary concern and automatic lifting alone would solve the
primary (call site / library user) concern.

Matthew

On Sat, Aug 20, 2016 at 12:35 Matthew Johnson <matthew@anandabits.com> > wrote:

> On Aug 20, 2016, at 10:36 AM, Haravikk <swift-evolution@haravikk.me> >> wrote:
>
>
>> On 19 Aug 2016, at 15:38, Xiaodi Wu via swift-evolution < >> swift-evolution@swift.org> wrote:
>>
>> Ad-hoc enums have been discussed already, at length, and all the
weaknesses touched on then still apply now. For instance, since they're
ad-hoc, can you pass an instance of type "Int | String" as an argument if
the function expects a "String | Int | Float"? Enums don't have duck typing
behavior like that; if your ad-hoc type does, then it's not very much like
an enum; if it doesn't, it won't feel much like a union type.
>
> While ad-hoc enums are certainly similar I don't think that this
problem applies to unions; the problem with ad-hoc enums is that while
cases may have the same name, the meaning of a case may not be identical,
so compatibility is uncertain. For type unions I'd say this isn't an issue;
I'd say that yes, String | Int is compatible with String | Int | Float as
every possible value can be carried over (whereas the reverse is not true),
they're just values of one of several types, so as long as the conversion
is possible, it should be fine to pass it on (or rather, repackage it
behind the scenes).
>
>> Moreover, an ad-hoc "String | Int" may look like a union type, but
until switching over an instance to cast it, you can't invoke any methods
common to String and Int. So it really doesn't feel like a union type at
all.
>
> Could it not do that though? I'd say that a union type should conform
to any common protocols that its members conform to; if this can be done in
the initial release then great, otherwise it can come later.

Conforming to common protocols would be much better than an implicit
ad-hoc / duck-typed protocol that simply exposes all common members. But
there is strong opposition to unions, much of which is related to
implementation complexity. It seems to me that the path to having unions
or a union-ish feature receiving serious consideration is to demonstrate
the value they can offer even with relatively restricted functionality
(such as syntactic sugar for enums with implicit lifting). If that is
successful we will have an opportunity to work with them and make a case
for enhancements in the future.

Also, it won't always possible for a union to conform to a protocol
conformed to by all member types. If the protocol has `Self` requirements
in argument position it would not be able to conform and if it has
associated type requirements which are bound to different concrete types in
the types making up the union it would also not be able to conform.

Matthew

That's good information. I guess a partly implied question which I'm not entirely sure of would be:

What are the scenarios you've encountered where it actually matters that an algorithm that works with any P & Q & R must ensure that the argument is one of a fixed list of concrete types?

Examples of String, Int, Bool call to mind JSON parsing, but in that scenario I'd expect the fixed list of concrete types would be a constraint of the parsing logic long before you get to any algorithm that operates on a P & Q & R.

I was simply responding to Haravikk offering some information on the inherent limitations of unions conforming to common protocols as well as my thought that it probably isn’t something to push for if we do officially revisit the topic of a union-like feature.

Nothing in my use case involves protocols at all! It is strictly defined in terms of the concrete types that are acceptable. String, Int, and Bool were picked arbitrarily for discussion. The specific context where I was experimenting with a DSL like this was a view configuration DSL where the types of supported child views are restricted to a specific list of concrete view types. But this same pattern could be applied to many DSLs that accept hierarchical configuration where some nodes would want to restrict the type of their children. I might be possible to build a JSON parsing DSL using techniques like this although I have not attempted to do that.

···

On Aug 20, 2016, at 1:29 PM, Xiaodi Wu <xiaodi.wu@gmail.com> wrote:

On Sat, Aug 20, 2016 at 14:09 Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:

On Aug 20, 2016, at 12:27 PM, Xiaodi Wu <xiaodi.wu@gmail.com <mailto:xiaodi.wu@gmail.com>> wrote:

Maybe I'm not getting something. But if you only want T | U | V to expose members required by common protocols P, Q, and R, since you know the types at compile time, you also know the common protocols. Why wouldn't you just write P & Q & R, and if necessary precondition(x is T || x is U || x is V)?

This isn’t something I personally was advocating for (in fact I specifically suggested *not* proposing this would be a better strategy for receiving serious consideration of something union-ish).

However, if we *did* do something like this it would provide a static verification that the precondition is met while still exposing the members of the common protocol(s) directly. That is substantially better than runtime verification of the precondition or the boilerplate necessary to expose provide forwarding members.

My personal interest in something union-ish is really along the lines of syntactic sugar for enums.

Rather than writing this:

enum StringOrIntOrBool {
    case .string(String)
    case .int(Int)
    case .bool(Bool)
}

func foo(values: [StringOrIntOrBool]) {}

foo([.string(“hello”), .int(42)])

I would prefer to just write:

func foo(values: String | Int | Bool) {}

foo([“hello”, 42])

The use case for this I found is in designing DSLs of various kinds where you need to accept a heterogenous collection of a specific fixed list of concrete types. The primary concern is to have static verification of the possible values provided by the caller without requiring callers to manually instantiate cases and without polluting the member types with unwanted conformance to a single-use protocol (which would be an alternative if we get sealed protocols).

If we add automatic lifting as Brent suggested that solves the primary concern I have. If we do add that, introducing the union-like notation would make sense as convenient syntactic sugar for use in these scenarios where the sugar states the intent more clearly and concisely than something a contrived name like `StringOrIntOrBool`. But as I have already stated, that is as secondary concern and automatic lifting alone would solve the primary (call site / library user) concern.

Matthew

On Sat, Aug 20, 2016 at 12:35 Matthew Johnson <matthew@anandabits.com <mailto:matthew@anandabits.com>> wrote:

> On Aug 20, 2016, at 10:36 AM, Haravikk <swift-evolution@haravikk.me <mailto:swift-evolution@haravikk.me>> wrote:
>
>
>> On 19 Aug 2016, at 15:38, Xiaodi Wu via swift-evolution <swift-evolution@swift.org <mailto:swift-evolution@swift.org>> wrote:
>>
>> Ad-hoc enums have been discussed already, at length, and all the weaknesses touched on then still apply now. For instance, since they're ad-hoc, can you pass an instance of type "Int | String" as an argument if the function expects a "String | Int | Float"? Enums don't have duck typing behavior like that; if your ad-hoc type does, then it's not very much like an enum; if it doesn't, it won't feel much like a union type.
>
> While ad-hoc enums are certainly similar I don't think that this problem applies to unions; the problem with ad-hoc enums is that while cases may have the same name, the meaning of a case may not be identical, so compatibility is uncertain. For type unions I'd say this isn't an issue; I'd say that yes, String | Int is compatible with String | Int | Float as every possible value can be carried over (whereas the reverse is not true), they're just values of one of several types, so as long as the conversion is possible, it should be fine to pass it on (or rather, repackage it behind the scenes).
>
>> Moreover, an ad-hoc "String | Int" may look like a union type, but until switching over an instance to cast it, you can't invoke any methods common to String and Int. So it really doesn't feel like a union type at all.
>
> Could it not do that though? I'd say that a union type should conform to any common protocols that its members conform to; if this can be done in the initial release then great, otherwise it can come later.

Conforming to common protocols would be much better than an implicit ad-hoc / duck-typed protocol that simply exposes all common members. But there is strong opposition to unions, much of which is related to implementation complexity. It seems to me that the path to having unions or a union-ish feature receiving serious consideration is to demonstrate the value they can offer even with relatively restricted functionality (such as syntactic sugar for enums with implicit lifting). If that is successful we will have an opportunity to work with them and make a case for enhancements in the future.

Also, it won't always possible for a union to conform to a protocol conformed to by all member types. If the protocol has `Self` requirements in argument position it would not be able to conform and if it has associated type requirements which are bound to different concrete types in the types making up the union it would also not be able to conform.

Matthew