[Pitch] Introduce existential `any`

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.

7 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.

What about using protocol or a new keyword for the first one?

typealias A = any P
protocol Q = P // or constraint Q = P
func useless(_ x: A) -> any Q { x }

If we’re taking on syntax churn anyway, I quite like the idea of existential type aliases and “protocol aliases” using different keywords.

1 Like

I do like the idea to have a distinct keyword for factoring out conformance requirements instead of using typealias, but I'm super mindful of crossing the line of imposing "too much" source change. My personal opinion is that the addition of any is fully justified (obviously, I am proposing it :slightly_smiling_face:) on the grounds of active harm that often causes developers to go down the wrong path and inevitably need to re-write a bunch of code to use generics instead. There's plenty of evidence that suggests this is the case.

I'm not at all convinced that using typealias for both conformance requirements and existential types reaches the "active harm" bar that justifies an additional source change. You cannot use these two type aliases in the same way after this proposal, and it's pretty straightforward to offer a fix-it for an invalid use depending on the context.

2 Likes

How is forcing the any keyword expected to get users to go down the right path from the beginning? Any user who doesn't know the limitations they'll hit with the existential going into it is unlikely to be dissuaded by having to use any. The generic syntax is still far more complex, so users will naturally reach for the simpler solution unless they already know it won't work. And when the user already knows one way or the other, this proposal just makes the existential syntax more complex for no gain.

3 Likes

I expect that folks will still try to write the bare protocol name instead of typing any P out of the gate. If the user types a bare protocol name P, SourceKit could offer a completion to insert a generic parameter conforming to P via code completion, and the same transformation can be offered through an error message fix-it. In the future, we can make this syntax "just work" as sugar for a type parameter conforming to P, so the thing that folks write naturally works, and it is using the abstraction tools that we want them to use by default. This future direction also solves the problem of generics still having a much more verbose syntax in cases where you don't need to refer to that generic parameter multiple times.

6 Likes