[Pitch] Introduce existential `any`

AFAIU, any T.Type is not a valid syntax, because bar<T> implies that T is a type. We would need a completely new feature to indicate that T is not a type, but a protocol - something like bar<T: Protocol> or bar<protocol T>.

Note that currently this is also not possible:

func bar<T>(x: T.Protocol, y: T.Type) {}
//               ^- error: cannot use 'Protocol' with non-protocol type 'T'

So the proposal does not change the status quo.

I don't view the necessity of a clear distinction between a super-type and a protocol conformance constraint on a syntactical level especially because of the idea of Generalized supertype constraints in the generics manifesto.

Therefore I would strongly argue that every R in a T : R relationship can be replaced with either a valid super-type of T or a protocol it should conform to.

Indeed, I think something like this would be able to express what you want:

func bar<T, U>(_ : T.Type, _: U.Type) where U: T {}
protocol P {}
struct S: P {}
bar((any P).self, S.self)

But that's a different feature, completely orthogonal to the proposal.

Your example remains illegal because S: any P is not a valid relationship as that's what your example decomposes to. :thinking:

Therefore one of my previous questions, would it make sense and or technically possible to having P.self return a new singleton metatype P.Type? This would solve this whole issue.

I think every time someone reached out to P.self, they actually expected to get something like P.Type (not to be confused with any P.Type), but instead they got confused because the resulting value was actually (any P).Type.

The proposal could make another breaking change to the language and introduce P.Type as a resulting value from P.self while it would also introduce the new, separate (any P).self from that returns (any P).Type (the old P.Protocol).

old new
T T.self T.Type T.Type
P P.self P.Protocol (new but a breaking change) P.Type
(any P) (any P).self P.Protocol (any P).Type

If we would keep P.self alongside (any P).self we will have a breaking change, but it would actually introduce a much broader fix to the language expressiveness.

One more thing, which seems to be not discussed yet. Does proposal change behaviour of String(describing: (any P.Type).self) and String(describing: (any P).Type.self) or keep them producing "P.Type" and "P.Protocol"?

2 Likes

Ok. Two new features then: one to support generalised super type constraint, one to create relation between S and any P. But still, IMO, it is orthogonal to the proposal.

Off-topic

I was starting a discussion about the second one, and will be happy to write a pitch after this proposal is approved. I think it provides a better alternative to self-conformance, and can help to eliminate self-conformance from the language. And also addresses use cases not covered by self-conformance. If you are interested in this topic, please comment in that thread.

If would not be a metatype, maybe we can call it a metaprotocol. Of type P.Protocol. Not to be confused with old P.Protocol aka (any P).Type! Ok, let's call it P.Constraint to avoid confusion.

Then I guess you want to write something like this:

func bar<T>(_: T.Constraint) {}
//               ^- error: cannot use 'Constraint' with non-protocol 'T'

So first, you need to express that T is a protocol:

func bar<protocol T>(_: T.Constraint) {}

But once you have it, you can write:

func bar<protocol T>(_: (any T).Type, _: T.Type) {}

which addresses your initial intention without T.Constraint. So my conclusion is that so far we don't need meta-protocols.

You'd need them to operate with protocols in the runtime. But currently I cannot think of any use cases.

I think this makes the situation much more complex that it really should be. As I mentioned previously in this thread, the "true" P.Type kinda already exists, but it's not obtainable or properly expressible. This metatype fix via the any keyword allows us to also fill this hole.

protocol P {}

func whatAmI<T>(_ t: T.Type) {
  print(type(of: t))
}

whatAmI(P.Type.self) // P.Type.Protocol
                        ^~~~~~

Make a breaking change, and let P.self actually return that P.Type instead while shift the previous P.Protocol to be the result of (any P).self and re-brand it as (any P).Type. Done.

People are constantly reaching out for P.Type but get (any P).Type instead because of all this metatype fusion madness. This is the right time to fix this, and I disagree that it's orthogonal to this proposal. Having the ability to actually get the singleton metatype P.Type would fix sooo many issues and confusion in the swift community.

This one extra thing unlocks an entire new set of generic algorithms. We can either ignore that or push this forward while we're already changing the status quo here. I've earned a lot of disagreement in this thread, just to see the core team join the conversation and make things clear to actually include the changes I've been fighting for the whole time.

P.S.: If you have Swift compiler know-hows, implement a quick and dirty proof-of-concept of this and see how awesome the expressiveness of the language becomes. It's a win win situation.

Could you please provide as an illustration a couple of algorithms generic over protocols? This would help discussion a lot.

The discussion here has convinced me this is the proper interpretation (thanks @DevAndArtist!), though I remain concerned about having these two spellings mean different things. I expect this will make it more confusing to elucidate the difference between the metatype existential and the singleton metatype, and certainly to tell them apart at a glance. To the extent that P.Type under this regime is a special syntax, I wonder if we could change the spelling to put a bit more lexical distance between these two forms. It would also avoid us having to explain why this .Type isn’t a real .Type (since it isn’t applied to a type). Not an actual suggestion, but something like any P.MetatypeExistential would make it clear that this is a different existential type related to P.

Overall, though, this is a pretty small issue and I won’t grumble about it any further, the pitch looks to be in quite good shape to me.

1 Like

Good point! Getting P from any P would be a useful feature for the reason you describe and hopefully there is room for this in future Swift. Since it's not possible to do today I agree that it could be left for a follow-on proposal, but it would be nice to have commentary on that point.

Could we not maintain the existing spelling P.Protocol and give it this new meaning (after a period of transition)? I for one would find that to be a more intuitive meaning for that spelling anyway.

I'm not a compiler engineer, but if I'm not totally mistaken then there exists an "existential metatype" for every type, not just for protocols (e.g. any Int.Type). While it's not that much useful as it can only hold Int.Type, I still think this is/was a thing. Feel free to correct me. In my previous attempts explaining this I was referring to the proposed (any T).Type as meta T and any T.Type as any meta T while also introducing another pseudo keyword meta. A more pleasant to parse form added typealias Meta<T> = meta T (or introduced a standalone Meta<T> form instead).

Here's a list of all the forms:

proposed alternative 1 alternative 2 old proposal alternative
singleton metatype T.Type meta T Meta<T> Type<T>
existential metatype any T.Type any meta T any Meta<T> AnyType<T>

One of the goals here, was to free up the .Type name space for custom types. I'm personally tired of calling everything Kind. :sweat_smile:

:point_up_2:this.

While I think I understand what you meant here, I'm not quite sure, because it seems to mix everything up again. P.Protocol is already taken as it will become (any P).Type under the current proposed revision. What I'm interested to hear from the compiler engineers or the core team, can we introduce the "true" P.Type (mentioned here) now?

IIRC I had a short conversation regarding this type with @Joe_Groff on twitter, but it was long long time ago. It was the point where I learned that this type is currently unavailable nor properly expressible in Swift.


  • P.Type - would be the singleton metatype of the protocol P
  • (any P).Type - would be the singleton metatype of the protocol existential any P

In general I like the idea to give P.Type in any P.Type a different spelling. Keyword any is already an indication of the existential, so IMO, word "existential" in the name is redundant. Instead, I would try to formulate the name so that it clearly describes what are the values of this type. Which leads me to a suggestion of any P.ConformingType. WDYT?


But regardless of the syntax, it got me thinking about what is P.Type in any P.Type. My understanding is that "existential type = any + constraint", so "existential type - any = constraint". Which leads to a question for @hborla, will P.Type be usable as a constraint under the proposal? Can I write something like this:

protocol P {}
struct S: P {}

struct Z<T: P.Type> {
    var value: T
}
let z = Z<S.Type>(value: S.self)

typealias PType = P.Type
func dummy<T: PType>(_ x: T) -> any PType { x }
1 Like

Yeah, MetatypeExistential was not meant to be a serious suggestion, just an illustration of the sort of change I was talking about. AFAIU, we make an effort not to use the 'existential' terminology almost at all—it receives only a single mention in TSPL as an aside. Something like ConformingType is a better serious suggestion. :slight_smile:

No, using P.Type as a conformance constraint in a generic signature does not fall out of being able to use it with any. Conceptually it might make sense, but today, existential types are not modeled in the implementation with generic signatures (they should be, because that would also allow for other constraints on existential types, e.g. any Collection<Int>, but that will require generalization of their representation in the type system).

1 Like

No. As I discussed above, the existential metatype for a generic type constraint Constraint is the type ∃ τ : Constraint . τ.Type. It does not apply (does not result in a well-formed formula) to things that are not known to be a generic type constraint, and since Swift does not allow abstraction over generic type constraints (e.g. struct A<Constraint: protocol> { func foo<T: Constraint>(t: T) }), that means it only applies to concretely named constraints. A concrete type like Int or NSObject or what have you only has a concrete metatype (which in the case of classes and class metatypes can also store subtypes).

2 Likes

As has been noted, having a representation of the "true" P.Type in the type system doesn't open up the ability to express new generic constraints, so I don't think we're actually unlocking a new set of generic algorithms. The only use cases I've been able to think up are around the idea of having a value of this "true" protocol metatype and doing a runtime check to see if a given type conforms to that protocol, sort of like as? does but without turning the result into an existential. However, as soon as I do that, I start wanting to use that "true" protocol metatype as a generic constraint.

It's not orthogonal to the proposal. If I understand correctly, it's not in conflict with this proposal, either. Rather, this proposal opens to the door for "true" P.Type as a potential follow-on, because it vacates the P.Type and P.self syntax, leaving it ill-formed in a future late version. That syntax could either remain ill-formed, or a subsequent proposal---with its own motivation---could use that newly-available syntax to do some of the things you describe.

Is that a fair characterization?

Doug

6 Likes

While I think I understand you, I had the impression that when we make the proper split between the singleton and existential metatypes we will also be able to express the compiler magic function type(of:) natively in Swift.

func type<T>(of instance: T) -> any T.Type // SE-0096

If that's still the case, what does type(of: Int(42)) return, when not any Int.Type which boxes Int.Type?

More than fair, thank you for providing some clarification on this.

The proposal does not change the split between singleton and existential metatypes. There is no new any Int.Type; that is ill-formed. The signature of type(of:) will still not be expressible in the type system because of its special behavior for expressions statically typed as existential.

The proposal introduces a distinction between naming a protocol or protocol composition as a constraint (which can be turned into an existential with the any keyword) or as an existential type (with the any keyword).

2 Likes

Forgive me for my persistence on this, I'm just confused right now. What if we actually allowed such any Int.Type boxes regardless of their usefulness? Would type(of:) function then be natively expressible in Swift? My brain still cannot process what the return type of the todays version of the same function is. Is that an Either like special type no one told us about? I always had the impression that it was a form of any T.Type where you could substitute T with any other type or constraint, even back then when @beccadax helped us polishing the related proposal.

:confused:

protocol P {}
struct Foo: P {}

let fooAsP: /* any */ P = Foo()

// existential metatype (any P.Type) boxing the
// singleton metatype Foo.Type
let a: /* any */ P.Type = type(of: fooAsP)

let b: /* any ??? */ Int.Type = type(of: Int(42))

If it's not the 'existential metatype', then type(of:) returns multiple different types? o.O
What is my misunderstanding here? :thinking:

It seems like you're saying that type(of:) only applies any to the return type when the passed parameter type itself has any, but why do we want that behavior? Is it that tremendously bad permitting any T.Type boxes where T can be anything?

To me this would significantly simplify the mental model here.

class C1 {}
class B1 {}
actor AnActor {}

let b1AsC1: C1 = B1()

let c: /* any */ C1.Type = type(of: b1AsC1) // boxes singleton B1.Type

let d: /* any */ AnActor.Type = type(of: AnActor())

let d: /* any */ (x: Int, y: Int).Type = type(of: (x: 0, y: 1))

I personally don't see any harm with this? If there's a potential performance issue, can't we somehow optimize this issue away still!?

I'm really scratching my head right now. :thinking:

type(of:) has different behavior when the operand is statically of existential type (including existential metatypes). To express the behavior of the function, you would have to overload it (infinitely, because existential metatypes can be infinitely nested) and introduce genericism over constraints:

func type<T>(of: T) -> T.Type // only eligible when T is not existential
func type<C: protocol>(of: any C) -> any C.Type
func type<C: protocol>(of: any C.Type) -> any C.Type.Type
func type<C: protocol>(of: any C.Type.Type) -> any C.Type.Type.Type // ad infinitum

That's just what it does. There is no spelling for this type relationship in Swift.