[Pitch] Introduce existential `any`

What exactly do you mean by this? Is this proposal consuming design space that would be required for a (separate) metatype-focused proposal, or is your concern that metatype issues won't on their own justify a source break and so would have to 'ride along' with the any proposal if the fixes require their own source breaks?

Well, as outlined before we have to be careful on how we involve any in the context out meta types. The proposed (any P).Type does already break the ideas from the UX discussion, as any P.Type was meant to become the "existential of the protocol P metatype".

If on the other hand we'd design it in an additive manner, then I'm fine deferring this topic. I have a fear that if we don't solve it with any at all, or if we go the with the pitched direction in the context of meta types, the chances on when we can resolve the issue with meta types might shrink down to zero.

Personally I would love to see a meta keyword and finally free up the .Type space for custom nested types called Type, however this is such a massive source break. :(

One future direction from our old proposal was this:

func subtype<T>(of type: Type<T>, named: String) -> AnyType<T>? { ... }

// with the idea of a `meta` keyword this would be
func subtype<T>(of type: meta T, named: String) -> (any meta T)? { ... }

// with the spelling from Joe Groff this would become
func subtype<T>(of type: (any T).Type, named: String) -> (any P.Type)? { ... }

I wonder if it could be as simple as allotting an entire major version (Swift 6) to be a transitional period where bare P-as-existential leads to only a warning with a fix-it.

There may be value in studying in detail how Rust made the transition also (I have not yet had opportunity to read the linked document that is undoubtedly illuminating).

3 Likes

Sorry, I probably should have written this in alternatives considered - this is a really enticing idea, and I tried really hard to make it work, but it leads to a pretty confusing programming model because you need to have a really deep understanding to know when you can write P and when you need to write any P.

This is true for function parameters, but it is not for stored properties. A stored let-constant with existential type cannot change dynamically for a single instance of that type, but the underlying type can certainly change across instantiations of that type, which means the type must be parameterized on that underlying type in order to preserve concrete type information, which is a much bigger semantic difference than using an existential type. These semantic changes also necessitate an ABI change.

That said, I really like the idea when I think about it with a different framing. What you're suggesting is effectively to be able to use a plain protocol name to mean an implicit type parameter conforming to the protocol. I don't think we should encourage people to think of this thing as a "special" kind of existential - people should think of it as a type parameter. This is what lead me to the future direction to allow this. It's effectively what we've been talking about doing with some, but not needed the some keyword.

I agree that a period of time when this is a warning with a fix-it would help with the transition.

5 Likes

Yeah, this is really only appealing to me if the "require any for all existentials" is considered too large of a break.

Ah, good point.

Cool, I like that a lot. If we can go the route of completely deprecating bare existential syntax, then reintroducing the bare protocol name as shorthand for an implicit, anonymous type parameter in Swift X1 would be great.

1: pronounced "Swift ten" :wink:

4 Likes

I've actually updated my previous message as I had things wrong interpreted by the pitch. I'm against the meaning of (any P).Type as proposed. This goes against:

And for more context on this issue here are a few examples:

protocol P {}
struct S: P {}

type(of: P.self)      // P.Protocol.Type

let p: P = S()
type(of: p)           // S.Type
type(of: p) is S.Type // true
type(of: p) == S.self // true

P.self is P.Type      // false
P.self is P.Protocol  // true

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

whatAmI(P.self)              // P.Protocol
whatAmI(P.Type.self)         // P.Type.Protocol
whatAmI(P.Protocol.self)     // P.Protocol.Type
whatAmI(Any.Type.self)       // Any.Type.Protocol
whatAmI(AnyObject.Type.self) // AnyObject.Type.Protocol

// True `P.Type` is not accessible today.

Here are some sub-type relations between existential types and also normal and existential metatypes. Let's say T is a non-protocol type and P is a protocol.

// For non-protocol types those types are sub-types of themselves.
       any T :        any T
(any T).Type : (any T).Type
  any T.Type :   any T.Type

// In case of protocols this is a bit different.
       any P :        any Any
(any P).Type : (any Any).Type
  any P.Type :     any P.Type

I know this topic is mind bending, but it is what it is and we eventually have to talk about it.

2 Likes

I’m of the understanding that the pitched meaning for (any P).Type is exactly what @Joe_Groff describes in the text you quote (“the type of the existential itself”). Indeed I cannot contemplate that spelling meaning anything else.

So long as that is agreed, everything else about changing metatype syntax can be a separate proposal—I would be strongly opposed to tackling any of that in one go as it’s an extensive topic of its own, as you well know.

A different quote a bit further up better illustrates the difference:

This proposal suggests that P.Type would become (any P).Type. Joe's previous suggestion was that P.Type becomes any (P.Type).

1 Like

@xwu "existential metatype" is not the same as "the (meta)type of an existential". The second and third quotes emphasis this more specific.

protocol P {}

// Joe's suggestion: `(any P).Type` vs. this proposal `P.Protocol`
let _ = P.self // "static" metatype

// Joe's suggestion: `any P.Type` vs. this proposal `(any P).Type`
let _: P.Type = ...
       ^~~~~~ // "existential" metatype

I am now thoroughly confused, but my read is that Joe has made several distinct suggestions, and what you quote is one that could be “if we say that P.Type is a generic constraint…” I do not think we need to say anything about that necessarily as part of this proposal.

Here is the only scheme my tiny mind can understand; I had thought that this was what was pitched:

  1. (any P).Type is the (meta)type of any P, the existential type.
  2. P.Type is an existing spelling; we do nothing with it.
  3. any P.Type is not part of this proposal, because P.Type is not itself an existential type (per item 2).

This would not preclude or limit the design space for any other examination or overhaul of metatypes so long as we are agreed on item 1, since the rest is just the status quo.


protocol P {}

// Joe's suggestion: `(any P).Type` vs. this proposal `P.Protocol`
let _ = P.self // "static" metatype

// Joe's suggestion: `any P.Type` vs. this proposal `(any P).Type`
let _: P.Type = ...
       ^~~~~~ // "existential" metatype

Sorry, I do not have the faintest idea what any part of this example means.

1 Like

I don't want this to sound like a drama, the proposal authors have to clarify this on us. It could just be a wording issue in the proposal where they actually meant to say "(any P).Type is the (meta)type of the protocol existential" instead of the "metatype existential" which are two different things.

In fact the "existential metatype" is probably better spelled as any (any P).Type.

I'm fine with the former, but against the latter.

If we're deprecating P as a type (for the time being) as part of this proposal, IMO we should also deprecate P.Type. It doesn't make much sense to me to have a P.Type when, indeed, P is not a type!

3 Likes

I had suggested in the pitch for the "existential metatype", i.e. the type that can store any concrete metatype conforming to the protocol, be spelled as (any P).Type. It made sense to me because of the way subtyping works with metatypes, but I can understand why folks would want this to be spelled as any P.Type. I agree with @Jumhyn that (any P).Type and any P.Type should not mean two different things because the syntactic distinction is far too subtle. In any case, I do not think that this proposal should change the .Protocol syntax, aside from perhaps making it (any P).Protocol.

1 Like

I agree with @xwu that this is the only meaning that makes sense to me for the (any P).Type spelling.

One alternative spelling that has been mooted in the past is Any<P>. Can we spare a few words in the Alternatives Considered section as to why any P is superior? Any<P> has the benefit of more clearly connoting the “existential box”.

2 Likes

Well I remain in a strong disagreement. This proposal says that "existentials" should receive the any keyword, however it mixes up what an "existential metatype" is and does not add the promised any keyword to metatype existentials themselves.

As I already said, I understand that this area of the language and discussion is extremely confusing to most of the readers. I spent days figuring out these tiny details. So I apologize for the caused disruption and confusing to everyone. However, it's on the table and I speak up, because I care about this topic, as I already did 5 years ago.

That said I would like to pivot the direction of this proposal to this:

// `T` is a non-protocol type, and `P` is aprotocol
let _: T = ... 
let _: P = ...
let _: any P = ... // already proposed


let _: T.Type = ...
let _: T.Type = ... // okay as it request `T.self`
let _: any T.Type = ... // explicitly requests the "existential metatype for T"

let _: P.Type = ...
let _: any (any P).Type = ... // the existential metatype of a protocol existential
1 Like

To be clear, I'm open to spelling the existential metatype as any P.Type, but I don't think (any P).Type should be valid and mean something different. I see three options:

  1. Allow (any P).Type and any P.Type to mean the same thing
  2. Only allow any P.Type and ban (any P).Type
  3. Only allow (any P).Type and ban any P.Type (currently in the proposal)

I'll take some time to process the discussion here, consider these alternatives, and write them out in the proposal. Thanks all for your feedback so far!

3 Likes

Two reasons. A minor one is symmetry with some P.

But more importantly, it would be misleading, implying that it's a generic type and that you could, in theory, write it yourself similar to how you can write Optional<T> yourself. But that's not true: this will be a built in compiler feature you would find very hard to replicate. any is like as or for... you can get close to constructing something similar, but fundamentally these are built-in language capabilities.

I am not sure this needs to be in "alternatives considered"... the word considered here needs to mean actually considered. Not "alternatives that could possibly be envisioned". This is kinda halfway.

13 Likes

What about the 4th option to allow (any P).Type) and any (any P).Type?

The latter is the "existential metatype of the existential protocol" where the former is the "metattype of the existential protocol".

So for non existentials (non-protocols) this would look like: T.Type and any (T.Type) or any T.Type for short.

To me this looks like sweatspot of this discussion so far.

metatype existential metatype
non-existential / non-protocol T.Type any T.Type
existential / protocol (any P).Type any (any P).Type

In that sense, any P.Type would be banned and provide a (any P).Type or any (any P).Type as a FIXIT.

This would allow us to fix type(of:) function like so:

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

And we could even introduce something like this in the future:

func subtype<T>(of type: T.Type, named name: String) -> (any T.Type)?
3 Likes

I'm strongly in favor of this, and I might not have been just a few years ago, so I think it might be worth outlining why.

There's a pattern I've seen several times now where a library author will design and implement their API, and then start iterating on performance optimization and discover that it's nigh-impossible to hit their performance targets without a major redesign.

If they had been undisciplined about performance ("oh it's fine, we'll ship 1.0 in a usable-but-slow state, and then optimize"), they might have found themselves having to deprecate and replace already shipped API, which is messy at best.

This is particularly common with people doing their first ObjC -> Swift switch (since ObjC assumes all values are pointer sized and respond to objc_msgSend, protocols and objects are equally [in]efficient), but it can easily happen to anyone.

Just like in cooking, a little syntax salt often helps the syntax sugar taste all the sweeter :slight_smile:

15 Likes