SE-0309: Unlock existential types for all protocols

Maybe it suffices to provide:

extension MyCollectionExtensionsProtocol where Self == any Collection

for custom extension methods.

Additionally, you just need to implement the type any Collection for MyCollectionExtensionsProtocol.

This is the same in Rust with dyn Trait implementing Trait, maybe this is the computationally better solution to retroactive protocols.

It is mentioned in proposal: "Allow constraining existential types, i.e. let collection: any Collection<Self.Element == Int> = [1, 2, 3]".

What about opaque type? The question is not directly connected to the proposal, but anyway.
Will it be possible in Swift 5.5 to write
let collection: some Collection<Self.Element == Int> = [1, 2, 3] ?

I don't see how that could work. Which concrete type is that supposed to be? How would the compiler choose? The array literal initializer has to invoke a constructor, but you've given the compiler no way to choose the intended implementation.

In Swift5.4, this works fine.

let collection: some Collection = [1, 2, 3]
print(type(of: collection)) // Array<Int>

Therefore, even if it were with constraint <Self.Element == Int>, perhaps it would still have type Array<Int>. I've never heard of such a constraint being enabled in Swift 5.5, though.

2 Likes

The literal itself has a type, the compiler just need to check if this type conforms to protocol Collection with associated type Element=Int

1 Like

The “ Improving the UI of generics” design document/submanifesto discusses this kind of constraint, with the syntax some Collection<.Element == Int>, but it’s not part of SE-0309 or any other pitch that’s been made so far, so nobody can say “yes, it will be available in <some version number>”.

3 Likes

First of all, although not directly part of SE-0309, the deeper discussion about existentials (and opaque types) has been extremely interesting to me, as it has views from both swift insiders and from outside. And it is relevant for the future features for existential, which were also highlighted in SE-0309. So thanks to all who have contributed.

Now this is interesting. Since it's the compiler, not the developer who makes the decisions about performance characteristics (speed vs code size, for example), then to me it's actually no longer important to advocate for "correct" choice to be made regarding generics vs. existentials, which for me was one important reason for "any P"... Especially now that SE-0309 has been accepted.

So I guess where "any P" (as one of the possible solutions), would be still be potentially relevant are the places where:

  1. you have to use generics, existentials wouldn't simply work. I guess SE-0309 implementation already has some error messages related to this, nudging you to the right direction?
  2. you have to use existentials, generics simply wouldn't work (the obvious one being heterogenous collections). Is there a way to better inform developers about this?
  3. future new features around existentials, and how to make syntax for those new features more understandable so that people get them right more often than wrong. One key place in my mind would be extension syntax for describing existential conformance to its protocol, which might be needed for protocols with associated types... if such thing makes sense:

extension any SomeProto: SomeProto { ... }

In a way I now find it understandable that the core team wants to defer the syntax change decisions to the proposals where new features would be introduced to existentials and then discuss the syntax in conjuction to those changes:

The current situation is not perfect, but at least we got rid of the overly eager firewalls in SE-0309 :slight_smile:

2 Likes

I haven’t seen anything related to dynamic casting or conformance checking is mentioned in the proposal, neither about existential metatypes.

Can someone clarify it for me, that if this feature is implemented, will we be allowed to write these codes:

let a = someObj is Hahsable
let b = someObj as? Hashable

let t = Hashable.self
let u: Hashable.Type
let v: Hashable.Protocol

Thanks a lot.

Yes, these examples will work.

1 Like

Yeah, the proposal allows any protocol to be used as a type, and that is exactly how protocols are used in these examples (apparently, we should mention this explicitly).

2 Likes

Thanks a lot!!
Really appreciate you guys' work.

1 Like

Regarding the discussion around terminology

I think the practical meaning lies in understanding the differences rather than trying to break down the terms. Perhaps the reason the term "existential" refers primarily to protocol types and their values in surface-level Swift is the dualism between protocol types, which are formally defined via existential quantification (there exists...), and generic types, formally defined via universal quantification (for all...). The term seems to be somewhat historically tied to protocol-like types, although some types and generic parameter types may also be defined via existential quantification.

A value of protocol type is just a run-time container that can store anything as long as it conforms to the protocol. The whole point of this run-time container indirection is that the type of its contents may change at run-time, and nothing is known aside from the existence of a conformance of the underlying (dynamic) type to the protocol (the identity of the underlying type is erased). As such, two distinct values of the same protocol type may simultaneously conceal values of different conforming types, as well as a single value of protocol type may conceal values of different conforming types at different points in time. The only thing that defines a protocol type is the protocol (the constraint that is imposed on the contents of the existential container).

// These are two values of the exact same protocol type, storing values
// of different conforming types. 
let int: Equatable = 0
var string: Equatable = ""

// The type of the contents can change dynamically.
string = true 

Consequently, it makes little to no sense to use a value of protocol type when the underlying type is known at compile time or the value is not expected to or cannot change (as in the case of int above or a function parameter).


some types are a purely compile-time concept. This means the underlying type is always known at compile time by design, eliminating the need for a run-time indirection. Despite the syntax, the identity of a some type goes beyond just the specified protocol and also captures the introducing declaration and any generic parameters of the surrounding context, thus preserving the identity of the underlying type.

func global1() -> some Equatable { 0 }
func global2() -> some Equatable { 0 }

func global3<T>(_: T) -> some Equatable { 0 }

// These are two values of *different* opaque types. 
// 'some1' has type 'opaqueResultTypeOf(global1)' (pseudo).
// 'some2' has type 'opaqueResultTypeOf(global2)'.
let some1: some Equatable = global1()
let some2: some Equatable = global2()

// These are two values of *different* opaque types. 
// 'some3' has type 'opaqueResultTypeOf<Int>(global3)'.
// 'some4' has type 'opaqueResultTypeOf<Bool>(global3)'.
let some3: some Equatable = global3(0)
let some4: some Equatable = global3(true)

The mapping from existential types to underlying types can be seen as a one-to-many relationship (every existential type corresponds to every conforming type), while the mapping from some types to underlying types can be seen as a one-to-one many-to-one relationship (every some type corresponds to a single conforming type).

2 Likes

Well, a many-to-one relationship, no?

1 Like

I disagree, to understand the differences one needs to define semantics for the terms first. To be concrete, what is an existential, a compile time existential, existential containers, existential values, existential types, existential box, opaqueness.

Then, differences between the meaning of these terms can be inferred and reoccurring discussions about them can be eliminated.

What does that mean?
Did you refer to the point of assigning an any P to some P?
And if some P types can be defined in terms of existential quantification, why aren't these existential types then?

Agree

Disagree, as this would imply some P != some P at the type level, which is a contradiction.
I think you are talking about three entities here:

opaque type: some P
underlying type transparent to the type system: (global1(),Int)
underlying type opaque to the type system: Int

And yes, between the "underlying type transparent to the type system" and "the underlying type opaque to the type system" we have a many-to-one mapping, e.g. Int can be mapped to type (global1(),Int) and (global2(),Int) but Float can't be mapped to (global1(),Int) as changing function global1 to return Float would also change the transparent type to (global1(),Float).

The mapping from some P to the "underlying type transparent to the type system" as well as to the "underlying type opaque to the type system" is one-to-many.
To see this, just change return 2 to return 2.0 in a function with some Number as return type and the function is still sound.

Mmm, some P != some P is in fact the fundamentally unique raison d’être of opaque types.

4 Likes

This is like saying: x != x or 2 != 2.
I think you mean underlyingTypeOf(var1:Some P=...) != underlyingTypeOf(var2: Some P=...) which can be true depending on the ....

Yes, it’s somewhat like .nan != .nan.

2 Likes

Yes, at the type level it's true*

let x: some Numeric = 2
let y: some Numeric = 2

x == y  // Binary operator '==' cannot be applied to operands of type 'some Numeric' (type of 'x') and 'some Numeric' (type of 'y')

*: unless your some Numeric refers to an already specified some Numeric

let x: some Numeric = 2
let y = x

x == y

y is still some Numeric, but it's some Numeric (type of 'x') and type identity works.

2 Likes

:/ Hmm..., good point.
Reminds me a bit of anonymous types/values where we create hidden tags to disambiguate them, i.e. make them false comparable
So we have:

x: some P => x: some Var(x).P
y: some P => y: some Var(y).P
some Var(x).P != some Var(y).P

Confusing, that a type is defined by its context.

That doesn't prove some Numeric != some Numeric as we don't compare types with each other. Equality isn't defined in general for some types but most of the time for the underlying type, so we have:

(Var(x),Int) for x and (Var(y),Int) for y which aren't comparable to each other because the Self behind some P mismatches.

While it’s very useful for you and other language theory oriented people to use very finegrained terminology for these things, I don’t think it fair to keep telling people are wrong when they use terms like opaque or existential with the meaning that is commonly used within swift community.

It’s like travelling to China and telling everyone they should all speak only English because that’s a language that you use.

I don’t know if I got the translations right, but I think:
Opaque type == ”underlying type transparent to the type system”
Existential == ”existential container type”

It would allow for more people to understand and also contribute to the discussion, if we don’t spend so much time trying to redefine the words themselves.

2 Likes
Terms of Service

Privacy Policy

Cookie Policy