[swift-evolution] [Manifesto] Completing Generics

Brent Royal-Gordon brent at architechies.com
Thu Mar 3 22:48:22 CST 2016


>> The choice of Equatable as an example for opening existentials is an
>> interesting one here, because it's one of the few cases I can think of
>> where differing dynamic types is actually fully defined: they're not
>> equal. In ObjC we express that by starting every -isEqual:
>> implementation with if (![other isKindOfClass:[self class]]) { return
>> NO; }, which while clunky and easy to forget, does neatly express the
>> desired semantics with no burden at the callsite.
> 
> It's a reasonable default, but it's not necessarily the right
> implementation for every type.  One could imagine Polygons comparing
> equal to Squares, for example.

If the "open" operation is sophisticated enough, when it's faced with mismatched concrete types but they both conform to a protocol with a existential that meets the requirements, it could return that existential.

That would mean that, in this scenario:

	protocol Shape: Equatable { ...}
	
	protocol Polygonal: Shape {
		var vertices: [Vertex]
	}
	struct Square: Polygonal { ... }
	struct Polygon: Polygonal { ... }

You could add this:

	extension Any<Polygonal>: Equatable {}
	
	func == (lhs: Any<Polygonal>, rhs: Any<Polygonal>) -> Bool {
		for (lhsVertex, rhsVertex) in zip(lhs.vertices, rhs.vertices) {
			if lhsVertex != rhsVertex {
				return false
			}
		}
		return true
	}

And then, if `==(_: Any<Equatable>, _: Any<Equatable>)` were passed a Square and a Polygon, opening the `Any<Equatable>`s would give you a pair of `Any<Polygonal>`s.

This is obviously more complex than simply opening the existential, matching its concrete type against a requirement, and extracting the original value if it matches—it's looking at the concrete types of N existentials, simultaneously matching them *all* against a requirement to find a more specific type they can all be cast to, and then performing that cast. To do its job, it would need to see all of the operands at the same time and evaluate them together to find a type that would fit all of them.

That's what I was trying to get at when I wrote this example earlier in the thread:

	func == (e1: Any<Equatable>, e2: Any<Equatable>) -> Bool {
		guard let concreteE1<T: Equatable> = e1 as? T, concreteE2 = e2 as? T else {
			return false
		}
		
		return concreteE1 == concreteE2
	}

If both parameters were `Square`s, then `T` would be a `Square`. But if one was a `Square` and the other a `Polygon`, `T` could be an `Any<Polygonal>`. Because you are simultaneously matching both values, you don't have to try the match both ways, and the operation is free to return a more specific protocol existential if that's the best it can do.

(And if there was no `Equatable` type they could be cast to that was more specific than `Any<Equatable>`, `T` would at least notionally be the bottom type and, since neither existential contains a value of the bottom type, both `as?` casts would return `nil`. `guard let` would then see those `nil`s and send you down the `else` branch.)

-- 
Brent Royal-Gordon
Architechies



More information about the swift-evolution mailing list