Existential subtyping as generic constraint

I've been thinking of Generic constraint for "weakly referencable".

struct WeakRef<T: AnyObject> {
    weak var ref: T?
}

class MyClass {}
protocol P: AnyObject {}

weak var x1: MyClass? // OK
weak var x2: P? // OK
var wr1: WeakRef<MyClass> // OK
var wr2: WeakRef<P> // ERROR: 'WeakRef' requires that 'P' be a class type

I understand why it does not work - because currently generic constraint T: AnyObject is a protocol conformance constraint - it requires type T to conform to protocol AnyObject. But existential type any P does not conform to that protocol.

But still, what I'm trying to achieve seems to be like a legit thing. any AnyObject can we declared as weak, both MyClass? and (any P)? can be upcasted to any AnyObject.

So, my conclusion is that protocol conformance constraint is not the constraint that I need. I need a subtyping constraint here. Currently it exists for references to classes, but not for existential protocol containers. Let's fix this!

struct WeakRef<T: any AnyObject> {
    weak var ref: T?
}

class MyClass {}
protocol P: AnyObject {}

weak var x1: MyClass? // OK
weak var x2: P? // OK
var wr1: WeakRef<MyClass> // OK
var wr2: WeakRef<any P> // OK: 'any P' is a subtype of `any AnyObject`.

PS. any AnyObject looks weird, it probably should be any Object.

1 Like

We'd need:

  • explicit existential syntax as any
  • potentials 'generalized super type constraint'

Some questions in reversed direction.

  • Will WeakRef<MyClass> still work and satisfy the constraint?
  • Is MyClass the same as any MyClass?
  • And what does any MyClass even mean?

Sorry, I was not clear about this. Since this one is already on the generics roadmap, for the sake of this discussion, let's assume it is already available and focus on the second one.

Yes. Because you can have var x: any AnyObject = MyClass().

My understanding is that any can be used only with protocols, so any MyClass should raise a type-check error, complaining that MyClass is not a protocol.

Well here is the thing. An existential is a box type with some automatic compiler magic. But I don't think the relationship MyClass: any AnyObject is valid though.

And I think we will potentially use any and some in other places other than just with protocols. Definitely if we're going to tackle metatype revamp with a meta keyword.

meta T: meta T         // subtype of itself
any meta T: any meta T // subtype of itself
meta T: any meta T     // subtype of the second meta type kind (there is an exception with existentials)
any meta T: meta T     // ⚠️ not possible

let type: meta T = T.self

any meta P: any meta P // subtype of itself
meta P: any meta Any   // P (existential) does not conform to `P` (the protocol)
meta P: any meta P     // ⚠️ not possible unless `extension any P: P` exists

But that's out of scope of the current discussion.

My whole point is that the subtype relation is not commutative, I believe.

any T: T // okay
T: any T // ????

The meta keyword is a new thing for me. Where can I read more about this? I'm gonna skip meta-related for now.

It is not. It is transitive (A: B, B:C => A:C), and reflexive (A:A), but not commutative. But I do not see yet why this is a problem. Could you elaborate on this?

Not sure how to read this. From any T, I assume that T is a protocol. Then any T: T is not a subtyping relation, but a protocol conformance one. And in general case it does not hold. And then T: any T either does not make sense if we strictly separate protocols from existential types, or it is the same as any T: any T, if using protocol in the type context means existential container type.

Probably that's not what you meant.

In my understanding, if X is a type, and P is a protocol, then X:P ⇒ X: any P, where : means protocol conformance relation in the first case and subtyping relation in the second.

1 Like

My understanding is the some keyword introduced with Opaque Result Types will be made available in more contexts as the work/need happens. I would say what you're trying to do would be a clear use case for it:

var ref: WeakRef<some P> //(with `any` potentially being used instead/also). 

I'm guessing it's somewhere on the road map, as to when it could land...? :man_shrugging:. This use case is also more complicated because you're wanting to use a some protocol as a concrete Type, so there may be other features the language will require to properly support such a thing.

Ah sorry, you are right, I actually had a brain fade.

If we would try to describe a subtype relation then it‘s actually the opposite of what I wrote.

// subtype: supertype
T: any T

Then we can theoretically upcast the subtype T to its supertype any T.

Just like this subtype relation:

T: T?

Which allows let t: T? = T().

Btw. since current let p: P already means let p: any P (in the future), I think that G<P> would also mean G<any P>. I could be wrong here, but otherwise I don‘t l know what the difference between those two types would be.

// subtype: supertype
T: any T

I'm still confused. Subtyping relation is relation on two types. Either T or any T is not a type.

  • If T is a type, any T does not make any sense. There is no such thing as any String.
  • If T is a protocol, any T is a type, but T is not a type.

Taking this into account, if T is a protocol T: any T means the same as any T: any T, and this holds - subtyping is reflexive.

This feature has its reasons behind it, but it causes a lot of confusion in understanding concepts. For the sake of clarity within this thread, let's disable it. Let's assume that you always need to write let p: any P and let p: P would give an error.

Given

protocol P {}
protocol Q: P {}
class C: Q {}
class D: C {}
struct S: Q {}

The following subtyping relations would hold, based on declarations

any P: Any
any Q: any P
C: any Q
D: C
S: any Q

And based on transitivity, we also get:

any Q: Any
C: any P
C: Any
D: any Q
D: any P
D: Any
S: any P
S: Any

And reflexive relations are trivial:

Any: Any
any P: any P
any Q: any Q
C: C
D: D
S: S

Note that C: Q and C: any Q are different relations. First one is a protocol conformance - it is a relation between a type and a protocol. The second one is a subtyping relation - a relation between type and another type.

EDIT: Also for every class C, both C: AnyObject and C: any AnyObject hold.

1 Like

Have you seen the generalized super type constraint section I added to the generics manifesto? This is what @DevAndArtist mentioned in his initial post. IIRC, @Douglas_Gregor mentioned that he didn't think this would be too large a task to implement. It should cover the use case you have here as well as several others such as function subtypes, optional, etc. I'd be happy to help with a proposal on this topic if you're able to pick up the implementation.

That’s exactly what I‘m talking about. Glad to see it is already there. It will be applicable to generic arguments as well, not only to associated types, right?

Would it also cover magical array covariance?

Also, I’d like to emphasize that protocol conformance constraint is not a subtyping constraint. Currently there is no subtyping constraint for protocols in the language, and the following sentence from the the manifesto is not correct:

Currently, supertype constraints may only be specified using a concrete class or protocol type.

1 Like

Yes, it would be applicable anywhere you can use a constraint.

Yeah, it would cover all of the magical covariance in the language (I’m not sure what the full list is).

Yeah, technically that is true, although in the presence of generalized existentials and using the current existential syntax in the language I’m not sure there would be an important difference. :wink:

The difference is that any P satisfies T: any P, but not the T: P. Even with generalized existentials any P still does not conform to P, right?

Ahh, sorry. I left self-conformance off of the list in my post which is also necessary for the correspondence. Self-conformance is something that isn’t possible for all protocols so you’re right that there is a fundamental distinction.

I don’t think I’ve thought about this before, but we won’t be able to spell existential subtype requirements until the any existential syntax is introduced even if generalized supertype constraints happen first. Yet another reason to make the move to the any syntax for existentials.

Can we get this included into Generics manifesto so that this moment is not forgotten? Shall I make a MR or what is the process?

A PR sounds right. My PR took a couple years to get merged, but you’d be making a correction rather than an addition so I think it would get merged a lot quicker.

Wouldn't "any MyClass" mean an existential to an object that is either MyClass or a derived class thereof? So it should make sense; maybe adding it if the compiler currently doesn't support it. (Maybe there should be a warning if the compiler can figure out the mentioned class is final.)

This sort of my view point as well. I don‘t know how it‘s implemented in a language to have a type which accepts a base type or all of its possible subtypes. I know that for protocols we have existentials, but why should it differ for classes? It could also be an object existential of some sort. So any Object is the same as Object today, just like we would get any P and P. Then I think we could also have some Object which is bind to Object for the user, but is a concrete subtype (or just Object) known to the compiler, for whatever API design reasons.

Just a minor nit, don‘t forget that there is extension any Error: Error {} already hacked into the compiler.

Compiler already supports that, and it simply MyClass. In a way, you can think of reference types as existentials. In contrast to C++, in Swift objects are not values, only references to objects are. So every time when class name is used in the type context, it actually means "type of reference to that class or any subclass".