SE-0309: Unlock existential types for all protocols

Where to use these keywords and when, needs to be carefully considered, indeed, since people don't understand the difference of behaviour that there is between existentials and generics, and that is made worse by the Swift's syntax not making enough difference between the two, causing people to make assumptions that they are the same or similar.

NOTE: below is my best understanding of the concepts, feel free to point out anything that isn't correct in regards to the way that Swift implements these concepts.

Both generics and existentials can be used in many places, as return type, as function parameter, or as property declaration, for example. Each of those uses have slightly different peculiarities, but for now let's just acknowledge that we are not only talking about return types with any P / some T.

Now, since "any" isn't in use in Swift yet, we don't have a formal description of what it's used for, but the idea so far has been to syntactically highlight existentials as any P, which means the protocol should be treated as a type, and to distinguish them from using a protocol as a constraint for some other type (i.e. a another type conforms to a certain set of rules). So the primary reason for that syntax is to separate protocols from protocols (existentials from constraints).

Generics and some T
Opaque types (where syntax is some T) are used in Swift e.g. as a return type. Their implementation is based on generics, i.e. locking down to a single type at compile time, which cannot be changed, and obviously doesn't change at run time either. You can think of compiler replacing the generics syntax with the actual concrete type at compile time and thus at runtime there isn't even any generic-related abstractions to be seen, which makes the code have very good performance.

Currently we have reverse generics with opaque type both in return type and as property

protocol P {}
extension Int: P {}
extension String: P {}

let value: some P = 1
func (value: Int) -> some P {
    return value
}

You could in the future have also some in function parameter position (possibly normal generics or reverse generics?)

func foo(value: some P) 

For example, if you pass 1 to the function foo, then only Int can be stored, and no changes can be done to that inside the function or at runtime.

so the word "some" here is meant to signify "a single concrete type from a set of possible types"

Existentials and any P
Existentials have type erasing behaviour, i.e. by nature those types only know about what is provided by the constraint (the interface defined by the protocol) and allows for using any of the types that conform to that interface to be used in that type. In case of collections with existential we typically talk about heterogenous collections i.e. collection does not need to contain single concrete type, instead it can contain many different types that just appear on the surface as the same existential. This means compiler keeps all the type abstractions around; both when compiling and at runtime, and this means the concrete type may change in the code, as long as it conforms to the constraints (i.e. the protocol). While this is less performant code, it allows you to have collections which can can contain many different types.

protocol P {}
extension Int: P {}
extension String: P {}

func foo(collection: any [P]) 

For example you could pass [1, "a", 3] to the function foo, and it allows using it, whereas in generics this would be an error.

so the word "any" here is meant to signify "any of the concrete types from a set of possible types"

EDIT: in a separate thread it was noted that having some as sugar for normal generics in function parameter might not make sense, but instead some would make more sense when used as reverse generics in that situation. Reverse generics for argument type? - #14 by xAlien95
I have updated this post to reflect those thoughts.

4 Likes