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

Brent Royal-Gordon brent at architechies.com
Fri Aug 19 01:20:17 CDT 2016


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

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



More information about the swift-evolution mailing list