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

Matthew Johnson matthew at anandabits.com
Fri Aug 19 09:06:56 CDT 2016


> On Aug 19, 2016, at 1:20 AM, Brent Royal-Gordon via swift-evolution <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> 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
> https://lists.swift.org/mailman/listinfo/swift-evolution



More information about the swift-evolution mailing list