Note on variance:
Variance
One primary use-case for constrained existential types is their the Swift Standard Library’s Collection types. The Standard Library’s concrete collection types have built-in support for covariant coercions. For example,
func up(from values: [NSView]) -> [Any] { return values }
At first blush, it would seem like constrained existential types should support variance as well:
func up(from values: any Collection<NSView>) -> any Collection<Any> { return values }
But this turns out to be quite a technical feat. There is a naive implementation of this coercion that recasts the input collection as an Array
of the appropriate type, but this would be deeply surprising and would bake the fact that Array
is always returned into the ABI of the standard library forever.
Constrained existential types will behave as normal generic types with respect to variance - that is, they are invariant - and the code above will be rejected.
This is not a brilliant example, because in the specific case of erasure to Any
, this is equivalent to simply dropping the Element
constraint. In other words, if the existential function returned any Collection
instead of any Collection<Any>
, the code should work.
Existentials are different to generic types in that you can choose whether or not to specify even basic parameters like the collection's Element
type. You can't create an Array<?>
or Set<?>
, where the value has its true type behind the scenes but is presented with an erased Array
or Set
interface, but you can with existentials.
Sometimes, developers reach to heterogenous collections in these cases - so, an Array<Any>
or Set<AnyHashable>
. This gives you the expected interface, where the Array's elements have been erased of their static types, but comes at quite a performance cost. In the example, creating a new Array with individually boxed elements is an O(n) operation which might allocate a lot of memory. By contrast, going from any Collection<NSView>
to any Collection
is a no-op.
When the compiler rejects the above code, specifically involving a same-type constraint to Any
, I think it would be valuable to emit a targeted diagnostic suggesting the constraint be dropped. It can be easy to overuse angle brackets, and developers might forget about this unique feature of existentials.
--
When it comes to non-Any
constraints, of course you can't simply drop the constraint. Consider a non-Any
example: returning a collection of integers as a collection of numerics:
func up(from values: any Collection<Int>) -> any Collection<Numeric> { values }
With the recent changes to generics syntax, it should be clearer about why this must fail today - because Numeric
is being used in a (conceptually, not syntactically) ambiguous way. There are two options for what the developer might be looking to express:
-
any Collection<any Numeric>
(heterogenous collection)
This expresses a very different concept to Collection<Int>
, and the problem here is more contravariance than covariance. Because <any Numeric>
is a same-type constraint, it implies that mutating
methods also accept any Numeric
s, even though a collection of Int
clearly doesn't.
As noted, this could theoretically be implemented by copying to a different collection (e.g. an Array<any Numeric>
) and boxing every element in individual existential boxes. But a lot of the point of using existentials is because you want a particular, specialised implementation, while presenting a convenient interface to clients that hides those details. I don't think people would be happy if the data could so easily be implicitly copied to an Array.
At the very least, I think it's a good idea for this to involve some kind of explicit Array initialiser call.
-
any Collection<some Numeric>
// any Collection<.Element: Numeric>
(erasure)
Theoretically you should be able to represent an any Collection<Int>
using either of these forms, and just like dropping the constraint, it should come at no cost.
But you can't express either these things right now anyway, and both the some
form and possible future generalised constraint shorthands are out of scope for this proposal.
So, to summarise: same-type constraints to Any
(or any looser type boundaries) are really for actual heterogeneous collections, not for erasure. If you want to erase types which are part of an existential, you can simply loosen the existential's constraints.
I think there is still room to solve this; the main issue with respect to this proposal is that it only adds same-type constraints, which are insufficient for the erasure cases where implicit coercion makes the most sense.
My reading of the proposal seems to suggest that it is some kind of failure or odd quirk, but IMO invariance is the only logical thing given the limited constraints being added here, and there is plenty of room to develop this as we add support for the latter syntax described above.