[Pitch] Introduce existential `any`

My take is that in the short term under this proposal, the fact that you even hit the speed bump of having to satisfy the compiler is the benefit you're seeking. With the current bare P syntax being accepted, the language never has an opportunity to guide users heading down the path of least resistance in a different direction.

By forcing users to write any P, the language is provided with an opportunity to intervene with appropriate diagnostics. The guidance for an attempted use of a bare P can be tailored for the specific case—in situations where generics would 'work' (e.g., using a bare P as a function parameter), we could offer a fix-it to convert the function into a generic one. Perhaps we wouldn't even offer a fix-it to convert the bare P to any P in cases where a generic conversion would also work, adding just a bit more friction to users attempting to go down the existential path.

By removing the spelling for existential types that looks just like any other type, we remove the current preference in the language for existential types as the 'default' type for protocols. We'd reclaim that spelling in the short term for intervention by the compiler to guide users in a potentially better direction, and in the long term we could evaluate whether a different default makes sense for the bare P spelling (such as shorthand for an implicit generic parameter, as mentioned elsewhere by @hborla).

8 Likes

I appreciate this clarification, thanks. I didn't really consider that way of thinking about it and perhaps the proposal should really work to sell this point in the motivation section because now I can see some value to the idea when presented in this way.

I'm personally unsure how to evaluate if the change is actually worth the potential pain of a source-break, but at least now it makes a bit more sense as to what the pragmatic tradeoffs actually are for day-to-day programming.

I understand what you're saying, but it's definitely not a "new feature" in several important ways, because the language today doesn't distinguish aliasing the protocol from aliasing the existential protocol type. If we start distinguishing them, we have to think about what it means to use an alias to a protocol in places that specifically need a type and not just an abstract protocol. Presumably most of them would just be required to use the any keyword, but as Adrian points out, that doesn't work for associated types; would typealias Assoc = /*bare*/ P be ill-formed if Assoc satisfied an associated type requirement? (We're only saved from having this problem with protocol declarations themselves because they cannot currently be a nested type and so cannot directly satisfy an associated type requirement.)

4 Likes

I also think that change will cause more harm ( source breaking changes where every app/library has to update the code ) than benefit ( that people will start using generics instead of existentials ).

I might be missing something, but I don’t see how would this teach developers when to use generic. It would still be easier to just add any in front of protocol, for example to type func run(a: any Runnable) rather than func run<T: Runnable>(a: T).

Developers who know the difference already use the proper form wherever makes sense, others who don’t know/care won’t be suddenly start using generics because of extra any keyword.

Maybe we can think of a different way to document and show best practices instead of causing source breaking change.

Apple could provide more information/guide on a website, have a dedicated chapter in swift book and maybe a tech talk where this is thoroughly discussed.
I know that it is already mentioned in old WWDC talk about Swift performance, but its might not be detailed enough.

As a developer I think it would be waste of my time if I had to go through all existential protocol usages and add keyword when I move to Swift 6, and then wait for dependencies to do the same, even with language compatibility mode it takes time and often has to be handled and tracked by apps which dependency to use which mode, like it was with 4/5 that still causes some problems today with managing dependencies.

2 Likes

I think logic dictates that this would be ill-formed except as a transitory measure.

Certainly I understand the point that currently users create “type” aliases for protocols for compatibility and related reasons, but since the guiding logic of the proposal is that it is desirable to distinguish protocols from their existential types in spelling, it cannot also be desirable to allow one type alias to meld the two back together. Therefore, if a type alias is to fulfill an associated type requirement, it must alias a type and not the bare protocol.

If being able to recover the bare protocol from the associatedtype-fulfilling typealias is essential, this would point to some need for the “inverse” operation of any that gives the protocol for a corresponding existential. (Using the deliberately horrible strawman syntax unany: given typealias Q = any P and typealias R = P, then Q == any R and R == unany Q aka unany any P.)

6 Likes

It took me a while to understand what you want to achieve here.

Using associatedtype with a protocol confused me a lot. If Swift ever gonna support this, IMO, it deserves a separate keyword - associatedconstraint or associatedprotocol.

Second mechanism is more intuitive for me, and it also eliminates the need for self conformance in many cases. I was proposing it here - Existential subtyping as generic constraint.

The language indeed supports such aliases. They can be created at runtime. For example, in the isP(existential:) invocation, the typealias A is an alias to the type of existentials (at least it quite looks like it does):

protocol P { }

func isP<T>(_ x: T) -> Bool {
    typealias A = T
    return (A.self is P.Type)
}

func isP(existential: P) -> Bool {
    isP(existential)
}

struct S: P { }
isP(S())              // true
isP(existential: S()) // false

But I'm still not aware of an existing way to create such a typealias in a static way, so I stand by my initial wording that allowing typealias A = any P would be a new feature.

I'm not sure I'm after a very important or relevant point, actually - it just looks like typealias A = any P can be made illegal by this proposal, if deemed necessary, without introducing any inconsistency or regression when compared to current language features. I'm well aware that @hborla and the core team are several steps ahead of me about the algebra of Swift types, so I'll carefully avoid having the slightest opinion about what the proposal should say about such typealiases.

Hello contributor and greetings community,

I take issue with the proposal on two fronts.

  1. The naming will make explaining to newcomers the difference between "any" and "Any" difficult. They are spelled the same with little to no relation to one another. They are entirely different concepts using the same word with different capitalization.
  2. More substantially, I don't see a concrete problem solved with the proposal.
    a. func useExistential(p: P) will still not be valid code, nor should it be. It could be expressed today as func useExistential<T, V>(p: T, value: V) where T: P, T.A == V.
    b. The other example function declaration you provided func test<T>(_: T) where T == any P likewise can already be expressed today in a more concise manner, namely func test<T>(_: T) where T: P

Perhaps I do not fully understand the problem, which is to say, blithely, the current syntax works for me or perhaps, don't fix something not broken.

I would appreciate if someone could please inform me what I am missing or overlooking in this case. Adding a keyword as sugar that could potentially confuse newcomers does not sound like a good idea in my opinion.

I acknowledge my bias in my current use of the Swift language as it stands. However, I fundamentally disagree that introducing sugar can ease a learning curve. some became a necessity due to SwiftUI, as I understand, and it's still a concept I rarely apply. I don't find that any can be compared to some because some landed as new functionality for Swift rather a source-breaking change, and along the same line, I still find there to be a learning curve to some, whereas there are many published guides available to writing code using generic constraints for protocols with associated types.

Thanks for your proposal Holly,

Ilias

1 Like

Regardless of whether SwiftUI motivated its inclusion in the language, some is a self-justified and standalone language feature. It gives an API vendor the flexibility to change the concrete type of a return value in an ABI-stable way, without introducing the indirection of dynamic protocol-based dispatch.

6 Likes

I don't think that's true. These concepts are related, and the way I'd explain them is this: if there was a protocol Any all types implicitly conformed to, it would be spelled as any Any. But this spelling would be redundant, and such protocol itself is too magical to be an explicit protocol in the standard library. Thus instead of any Any we have a simple Any existential type, for which all other types are subtypes.

In fact, from certain perspective I would even prefer the explicit nature of any Any. That would indicate that boxing is happening, which has an impact on performance.

This keyword is not sugar, but the opposite (syntactic salt?). The lack of any was (still is, unfortunately) syntactical sugar that allowed creation of existential types by using plain protocol names as a shorthand. I find that the lack of this keyword is what confuses newcomers the most in this aspect of the language.

8 Likes

They are in fact the same concept: Any is an existential with no constraints. (Once upon a time, protocol composition was spelled protocol<P1, P2> instead of P1 & P2, and Any was a typealias for protocol<>.)

I do think it would be easier to understand this if we change the spelling of Any to any somethingOrOther, but I don’t have a clear proposal for what somethingOrOther should be – Value has been suggested, but that’s confusing given that we often talk about “value semantics” applying to a subset of language values.

5 Likes

How's about any Type?

1 Like

I suppose I missed a point in case what you are saying is correct. I take it func useExistential(p: P) would become func useExistential<T>(p: any P, v: T) in order to pass a parameter to the type conforming to P. That sounds like an improvement over what I had before, useExistential<T, V>(p: T, value: V) where T: P, T.A == V. My doubts are mainly then whether the compiler could type check that sort of expression.

1 Like

In my understanding of the proposal this is incorrect. func useExistential(p: P) would become func useExistential(p: any P).

useExistential would not be able to call test(a:) in that case, which is the only thing declared on the protocol P. I don't understand the purpose.

Or we could go the other way, not introduce a new keyword, and just embrace where the language needs to go anyway, which is to have generalized constraints on all existentials. In other words, Any where some-constraints is almost guaranteed to become a legal syntax. That would imply the existential for protocol P could be spelled Any where Self: P, which I think is self-explanatory. I'm not sure we need anything else, in fact. If you don't like the verbosity, you can always create a typealias.

2 Likes

I don't know that I would say that that's almost guaranteed to become a legal syntax, because it's really quite problematic. There are already a number of places in the Swift grammar where a where clause can follow a type, most importantly the return type of a function:

func <T>(x: T) -> Any where ... /* what does this clause apply to? */ {
  ...
}

We can arbitrarily disambiguate this in favor of the current interpretation, forcing programmers to write the other with parentheses, but it seems to me that we really just shouldn't add a syntax which leads so easily to this sort of problem.

5 Likes

Okay, point taken… but I think if you accept that generalized constraints on existentials are an important feature, we're going to run into that problem with the any (lowercase) proposal as well. In a way this issue reinforces my point that we should be thinking about the long term shape of the language rather simply trying to resyntax what's currently there.

5 Likes

I would like for it to be possible to make type aliases to any P invalid, but I don't think it is. If you cannot write a type alias to an existential type, then there's no way to express that an associated type requirement is satisfied with an existential type, which is something that you can do today:

protocol P {
  associatedtype A
  var value: A { get }
}

protocol Q {}

struct S: P {
  var value: Q { ... }

  // 'A' is a typealias for 'Q'
  func test(arg: A) { ... }
}

With the addition of any:

protocol P {
  associatedtype A
  var value: A { get }
}

protocol Q {}

struct S: P {
  var value: any Q { ... }

  // It only makes sense for 'A' to be 'any Q'
  func test(arg: A) { ... }
}

I agree that the difference between typealias A = P and typealias A = any P is unfortunately subtle, but I don't see how to avoid it in a way that still achieves the goal of the proposal to distinguish a protocol's generic conformance constraint from its existential type.

2 Likes

While I agree that spelling Any as any somethingOrOther would be easier to understand, I think somethingOrOther should not be an identifier, otherwise it looks like an existential for a protocol, while, as you said, it is an existential with no constraints. As a vague idea - something like any ∅, but I don't really have a good idea for an ASCII spelling.