Summary
The type of a Swift existential is currently written as simply the name of the protocol. There have been a few suggestions of better ways to spell the name of an existential; I propose that we should introduce Any<MyProtocol>
as the preferred spelling of protocol existentials.
Specifically, I suggest that we should start accepting Any<MyProtocol>
as a preferred way to declare existentials soon, even before we add any additional features to existentials. We would still support the current spelling for source compatibility reasons.
Background
We frequently receive questions from people confused about the limitations placed on "variables of protocol type". (We tend not to use the word "existential" in compiler error messages, but that's what they are.) I believe many of these questions come from an incorrect understanding of how existentials work, and that by clarifying their spelling, we can make the language easier to learn and easier to use. This proposed spelling also opens up some opportunities for future features.
An existential is a wrapper type which can hold any value conforming to the protocol. For example, var anyAnimal: Animal
below is an existential.
protocol Animal {}
struct Cat: Animal {}
struct Dog: Animal {}
// A variable of type 'Animal' is an existential; it can hold *any* Animal
var anyAnimal: Animal
anyAnimal = Cat()
anyAnimal = Dog()
The existential wrapper box type is mostly invisible to the user. For example, type(of: anyAnimal)
returns the contained in the box. However, the wrapper is still a separate type. This can be observed when interacting with generics:
func takeAnything<T>(_ x: T) {
print(T.self)
}
let dog = Dog()
let anyAnimal: Animal = Dog()
takeAnything(dog) // prints "Dog"
takeAnything(anyAnimal) // prints "Animal"
This also comes up when users try to pass an existential box to a generic function constrained to that protocol:
func takeASpecificAnimal<T: Animal>(_ animal: T) {}
// error: value of protocol type 'Animal' cannot conform to 'Animal'; only struct/enum/class types can conform to protocols
takeASpecificAnimal(anyAnimal)
The problem here is that a "value of protocol type 'Animal'" (i.e. the Animal existential box type) is a separate type from Dog
or Cat
, and that separate type does not conform to Animal
on its own. This function is asking for a specific kind of animal -- a concrete type, not an existential wrapper.
The Problem(s)
There are two related problems which I think we can address in the short-term:
-
Many users do not have a clear understanding of the difference between protocols and existentials, and the language doesn't help them.
Because Swift uses the same spelling for both the protocol and the existential wrapper, the language does not help users build a mental model that differentiates the two concepts.Developer experience with other may not be very helpful either. In Objective-C, for example, all object variables were basically of the same type. When you used a
Dog *someDog
variable or parameter, it was still just passed as an object, exactly the same wayid<Animal> someDog
would have been. There was no "existential box" type. Swift doesn't help users understand that there's a separate box type involved. -
The specific error message about "values of protocol type" when used with generics is not actionable to many users.
Since many users don't understand thatvar anyAnimal: Animal
is a wrapper type around an Animal, they may be confused by the error message. They may belive that sinceanyAnimal
is currently a Dog, and Dog is a struct, the error message is incorrect.This is arguably just a specific instance of problem #1, but it's one that comes up frequently. We've gone through a few different iterations on this particular error message, but confusion persists.
Proposed solution
We already have a spelling for an existential type when no protocols are involved: Any
. Most users seem to understand Any
as a wrapper type that can hold any value. We should leverage that familiarity to help users understand existentials by preferring the spelling Any<Animal>
for a constrainted existential. When the protocol is used as a constraint, it continues to just be the protocol name. When used as an existential, we prefer the Any<MyProtocol>
spelling.
With this spelling, the above code would look like this:
protocol Animal {}
struct Cat: Animal {}
struct Dog: Animal {}
var anyAnimal: Any<Animal>
anyAnimal = Cat()
anyAnimal = Dog()
let dog = Dog()
func takeAnything<T>(_ x: T) {
print(T.self)
}
takeAnything(dog) // prints "Dog"
takeAnything(anyAnimal) // prints "Any<Animal>"
We can also improve the error message in the generic case:
// error: value of type 'Any<Animal>' cannot conform to 'Animal'; only struct/enum/class types can conform to protocols
or even:
// error: 'Any<Animal>' does not conform to protocol 'Animal'
This makes it obvious that the existential wrapper type is a distinct type from the protocol itself, and is also distinct from Dog
or Cat
. Dogs and Cats can conform to a protocol, but Any<P> cannot.
Backward compatibility
We can't just break existing code; we'll need to continue to support the existing spelling. I think we should treat it as an shorthand, though; using a protocol P
as a type would be an alias for Any<P>
. Autocompletion should prefer Any<P>
where possible.
What about `any P`
What about any P
?
The Improving the UI of generics thread from last year contemplates using any P
instead of Any<P>
as I've suggested here. There's a nice symmetry between any P
and some P
as introduced in Swift 5.2's opaque return types. However, I think we need to choose between analogy with Any
and analogy with some P
. I think the analogy with Any
is the better analogy. A few points:
-
We already have
Any
as an unconstrained existential wrapper. I anticipate fielding questions about the difference betweenAny
andany
in Swift, and I don't know that I'd have a satisfactory answer. They both introducea wrapper type capable of holding any conforming type. -
An existential wrapper is a new type. The analogy with
some P
is weak here, becausesome P
doesn't introduce a wrapper type, so it's not obvious thatany P
would do so.Any<P>
looks much more like a distinct type, which helps users understand the fundamental concept that an existential is a separate type. -
The generics manifesto also contemplates the ability to add methods to the existential type itself, using syntax like this:
extension any Animal { func rest() {} }
To me, this syntax is quite confusing; the naive reading suggest that the extension methods defined here can be called on any Animal type, when in fact the exact opposite is true; they would not be available on
Dog
orCat
, but only on the existential type itself.On the contrary, using
Any<Animal>
seems to make it much more clear that the extension is only on the existential type itself.extension Any<Animal> { func rest() {} }
Of course, we don't support extensions on existentials at all right now, so maybe this is a moot point. But if this is a direction we plan on exploring in the future, I'd rather be set up for the one that seems (to me) more natural.
Anyway, this has gone on long enough. I think we could get a pretty good win by simply introducing a new preferred spelling of our existing existentials. I think it would be minimally disruptive, composes well with concepts for future expansion, and resolves a common point of confusion among users. I'd love to hear your thoughts.