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

Matthew Johnson matthew at anandabits.com
Fri Aug 19 10:10:13 CDT 2016


> On Aug 19, 2016, at 9:38 AM, Xiaodi Wu <xiaodi.wu at gmail.com> 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.

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 Fri, Aug 19, 2016 at 9:07 AM Matthew Johnson via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
> 
> > On Aug 19, 2016, at 1:20 AM, Brent Royal-Gordon via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at swift.org>> wrote:
> >
> >> On Aug 18, 2016, at 2:05 AM, Maximilian Hünenberger via swift-evolution <swift-evolution at swift.org <mailto:swift-evolution at 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 at swift.org <mailto:swift-evolution at swift.org>
> > https://lists.swift.org/mailman/listinfo/swift-evolution <https://lists.swift.org/mailman/listinfo/swift-evolution>
> 
> _______________________________________________
> swift-evolution mailing list
> swift-evolution at swift.org <mailto:swift-evolution at swift.org>
> https://lists.swift.org/mailman/listinfo/swift-evolution <https://lists.swift.org/mailman/listinfo/swift-evolution>

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160819/18a323e7/attachment.html>


More information about the swift-evolution mailing list