I'm excited about making existential explicit, so +1 on the general direction.
I've raised the suggestion of Any<P>
in the past, and I appreciate it was addressed in the proposal. I suspect that the decision is pretty much made on that front already, so I'm hesitant to dive in again, but I did want to address the comments in that section.
From the Alternatives Considered section of the proposal:
Use Any<P>
instead of any P
A common suggestion is to spell existential types with angle brackets on Any
, e.g. Any<Hashable>
. any P
has symmetry with some P
, where both keywords can be applied to protocol constraints. The Any<P>
syntax is also misleading because it appears that Any
is a generic type, which is confusing to the mental model for 2 reasons:
- A generic type is something programmers can implement themselves. In reality, existential types are a built-in language feature that would be very difficult to replicate with regular Swift code.
- This syntax creates the misconception that the underlying concrete type is a generic argument to
Any
that is preserved statically in the existential type. The P
in Any<P>
looks like an implicit type parameter with a conformance requirement, but it's not; the underlying type conforming to P
is erased at compile-time.
All of this is true, but it doesn't convince me that any P
is better. To respond to some points directly:
any P
has symmetry with some P
, where both keywords can be applied to protocol constraints
I view this symmetry between some P
and any P
as a source of confusion, not clarity. While it's true that both some
and any
are keywords that can be applied to protocol constraints, that similarity hides an underlying dissimilarity, which is that any P
creates a new distinct wrapper type:
func useSome() -> some Numeric { return 42 }
func useAny() -> any Numeric { return 42 }
let tSome = type(of: useSome()) // Int
let tAny = type(of: useAny()) // any Numeric
An existential is a wrapper type that can hold any value conforming to the protocol. The fact that any
creates a new type while some
just elides an existing type is an essential distinction between the two, and the fact that they are both keywords only serves to hide this fact. A new type should look like a new type. Any<P>
looks like a new type. any P
does not.
The Any<P>
syntax is also misleading because it appears that Any
is a generic type.
And any P
is misleading because it doesn't appear that any P
is a separate wrapper type at all.
[this is] confusing to the mental model [because]:
- A generic type is something programmers can implement themselves. In reality, existential types are a built-in language feature that would be very difficult to replicate with regular Swift code.
Any
is already magical; you already could not implement Any
in regular Swift code. I don't see how Any
growing some angle brackets would make users suddenly think it was implementable in regular Swift code, nor is it clear to be why the presence of angle brackets would make users want to implement it in regular Swift code.
- This syntax creates the misconception that the underlying concrete type is a generic argument to
Any
that is preserved statically in the existential type. The P
in Any<P>
looks like an implicit type parameter with a conformance requirement, but it's not; the underlying type conforming to P
is erased at compile-time.
Yes, Any<P>
would act differently than a normal generic type. Any
is already magical, though, so I'm not sure what practical confusion this would cause. Is the objection that someone would try to write code like this, and it wouldn't work?
func takeAny<T>(_ thing: Any<T>) {
print("You passed me some sort of \(T.self)")
}
I suppose that's a fair objection; if that's the concern, I think it would be useful to call that out directly, because I didn't see that until I sat down to write this and thought about it for a while.
Since I'm suggesting that Any<P>
would not actually be generic, we should take off the <T>
on takeAny<T>
. Then it would look like this:
func takeAny(_ thing: Any<P>) {
print("You passed me some sort of \(P.self)")
}
But then the compiler is going to give a warning that it doesn't know what P
is. What the user actually needs to do is this:
func takeAny(_ thing: Any<Numeric>) {
print("You passed me some sort of \(Numeric.self)")
}
Does this adequately capture the concern you have regarding the Any<P>
syntax? My response would be that Any
is already magical and I think it would be okay for it to continue to be magical in this regard, but I can understand better now what the objection is. I would be open to considering other syntax for wrapper types if it would help address the concern over confusion with generics, such as Any(P)
or Any[P]
, but I suspect there's not much appetite for that. I do still think that we could address the concerns above with compiler diagnostics.
TL;DR
The crux of my argument is that an existential is a wrapper type, and it should look like a wrapper type. Currently it does not. I think that Any<P>
looks more like a wrapper type than any P
does, and I worry that switching from func x(_ p: P)
to func x(_ p: any P)
will not adequately convey to users that p
is a wrapper type, not the original type. Understanding that it's a wrapper type is crucial to understanding why any P
does not itself conform to P
, and I worry that we're not adequately communicating that to users.