[Pitch] Introduce existential `any`

That's and interesting idea.

My intuition was that typealias is used for types, so for everything declared as typealias you can do MyP.self and MyP.Type and RHS of the typealias must evaluate to type value. Bare P is a value, but not of the type kind. That's why above I was suggesting a separate keyword for constraint MyP = P.

any P is a type value. You can do (any P).self and (any P).Type.

On the other hand some P becomes a type expression only together with the declaration it is used on:

func f() -> some P { 42 }
func g() -> some P { "abc" }
let x = f() // x has type which is described as (some P, return type of f), which evaluates to Int
let y = f() // y has type which is described as (some P, return type of g), which evaluates to String

Bare some P without a declaration is not even a type expression. I guess you can call it a notation.

So if allow some P as RHS of typealias, then typealias becomes kind of a macro definition. It stores a chunk of AST which may or may not represent a value of some kind.

IMO, that goes against the goal of making distinction between protocol-as-type and protocol-as-constraint for developers. But I'm not strictly opposed to the idea. My point is rather that we need to have a discussion and clarify the meaning of the typealias keyword - do we want it to be let for types or a macro-like multi-purpose declaration.

3 Likes

I would appreciate the proposed solution to be applied to the example.

(A nice solution would be a path dependent type p.A, but that would probably be another proposal).

I really donā€™t think we should. any P always refers to the same type, but some P is a factory for new types. Allowing func f() -> MyP3 and func g() -> MyP3 to have different opaque return types would be immensely confusing, and making them the same opaque type would introduce a form of global type inference.

2 Likes

I want to know why we should.

People seem to assume some relationship between some and any. However, there is no document that explains that relationship. Is there constraint like 'if an expression can be used for any, the expression must be also able to be used for some, and vice versa'? (e.g., if typealias MyP = any P is possible, then typealias MyP = some P should also possible`)
Could you please tell me?

Both are still types, any - existential (meta-) type and some an opaque type / reversed generic type. typealias and associatedtype should support all kind of types, regardless if it's a (reversed) generic type or not. Why should I not be able to give my (opaque/existential (meta-)) type an alternative name? If we should be able to eventually nest opaque types in generics, shouldn't it also apply to generic type aliases?

typealias G<T> = T
typealias SomeP = G<some P>

@jayton I do not disagree on the potential confusion part, but that's always a subject of the current context. There are other valid use cases for a typealias such as to extract the long type name into its own short declaration, possibly, inside some exclusive scope due to things like character length per line in a given code base.

I'm sorry if the reply sounded like a rebuttal. I just wanted to ask you this point.

  • If we were to allow MyP2
  • then we should also allow typealias MyP3 = some P

Your explanation is of its usage and usefulness. It is the reason for the latter point, but not for the former point. How the existence of typealias MyP2 = any P involves in allowing typealias MyP3 = some P?
My intention of asking was this:

Yes, and people do this, for example, when they want to rename a protocol while keeping the old name around, but deprecated.

In such a renaming scenario, the goal is not to create a typealias for the existential, of course not. The goal is that both names are 100% compatible, so that the renaming is not source-breaking:

-public protocol OldName {
+public protocol NewName {
   ...
}
+@available(*, deprecated, renamed: "NewName")
+public typealias OldName = NewName
// Keeps compiling, with deprecation warnings:
struct S: OldName { ... } // protocol conformance
func f<T: OldName>(_ t: T) { ... } // generics
func f(_ t: OldName) { ... } // existentials
func f<T>(_ t: T.Type) {
    if let pt = t.self as? OldName.Type { ... } // runtime check for conformance
}
// etc.
5 Likes

No worries. There is no direct implication from any types being permitted inside a type alias that can be applied to some other than my own wish for symmetry in the type system.

1 Like

No. Bare some P without extra context is not a type. Similarly to regular generic types, you can make typealias for reverse generic type only within generic context:

func f<T>(x: T) {
    typealias U = T // OK, T is a type here
}

typealias U = T // But what is this supposed to mean?
typealias MyP = some P // Same thing

func g() -> some P {...}
// Typealias for reverse generic in its context would look something like this:
typealias TypeOfG = typeof g()
3 Likes

I would argue that func g() -> MyP would resolve the aliased opaque type at the point of use in for an opaque type valid position. I'm fine in being wrong here. That's just my intuition. ;) Feel free to correct me.

Right, exactly: the programmerā€™s intent is usually to make an alias for the protocol generally rather than specifically for the existential type. I think this naturally leads towards a model where whether the protocol was any-ed in the typealias is semantically relevant. If the ptotocol is any-ed in annalias declaration, then you wouldnā€™t need to use any where you write the alias name, but the alias name also wouldnā€™t be valid as a generic constraint. Otherwise, it would behave exactly like a protocol name. This is what Iā€™m worried might be too subtle, though.

We do need to resolve this question about typealias now if we want to give new meanings to bare protocols in the future.

5 Likes

some is not abstractable in the same way because the dynamic identity of the type (and therefore its static independence) is derived from exactly where and how many times the some keyword was written. For example, a function returning (some P, some P) may return a tuple of different types, but if it were (MyP, MyP) where MyP is an opaque alias, it would have to be homogeneous.

The other part is that the compiler does have to be able to uniquely identify a dynamic identity for the type somehow. I think it would be possible to force it to be uniquely determined by some sort of whole-file (whole-module?) consensus, but thatā€™s a lot of new language and implementation complexity.

The overarching point here is that some is special and needs to be considered separately.

6 Likes

This is a pretty good question ā€” Iā€™m wondering the same thing. I feel like we need a pretty compelling story here for the cost of the source-break to be worth it.

Would appreciate hearing what folks think about this.

2 Likes

Maybe that question is, precisely: "Do we want the language to allow defining a typealias for the type of existentials?"

The syntax for such aliases would be, I presume, typealias A = any P (and derivatives for more complex existentials). The compiler is entitled to accept this syntax, or to reject it, and the choice is still open.

Unless I'm mistaken, this would be a new feature, because defining such an alias is impossible today: P is the protocol, not the type of existentials, when used in typealias definitions.

Do we have use cases for such a new feature, "typealias for a type of existentials"?

Edit: Found and updated my super old example example that would use an any-ed type alias and some other feature from the generics manifesto.

protocol DelegateProtocol: AnyObject {
  func foo()
}

protocol DelegateContainer: AnyObject {
  // generalized super-type constraint:
  // require that `ConcreteDelegateExistential` is an existential which
  // stores a concrete type (constraint) `ConcreteDelegate` that conforms 
  // to `AnyObject`, and NOT that `ConcreteDelegateExistential`  itself is
  // the `AnyObject` conforming concrete type
  associatedtype ConcreteDelegate: AnyObject
  typealias ConcreteDelegateExistential = any ConcreteDelegate
  var delegate: ConcreteDelegateExistential? { get set }

  // alternatively we could also just write
  var delegate: (any ConcreteDelegate)? { get set }
}

class SomeClass: DelegateContainer {
  // `ConcreteDelegate` is linked to `DelegateProtocol` which makes 
  // `ConcreteDelegateExistential` equal `any DelegateProtocol`, or in
  // other words an existential which stores a type that conforms to 
  // `DelegateProtocol` and `AnyObject`
  typealias ConcreteDelegate = DelegateProtocol
  var delegate: ConcreteDelegateExistential?
}

class Delegate: DelegateProtocol {
  func foo() {
    print("foo")
  }
}

func connectDelegateContainer<T>(
  _ container: T,
  with delegate: AnyObject
) where T: DelegateContainer {
  if let delegate = delegate as? T.ConcreteDelegate {
    // store the concrete delegate instance inside the existential
    container.delegate = delegate
  }
}

let container = SomeClass()
let delegate = Delegate()

connectDelegateContainer(container, with: delegate)

container.delegate?.foo()

Another way would be to require associatedtype ConcreteDelegate: any AnyObject, which would result in typealias ConcreteDelegate = any DelegateProtocol on SomeClass. This type of constraint requires a concrete existential which stores a type that conforms to AnyObject. This does also show why it coudld be that in the future we might need both type of constraints T: Object and T: any Object where Object refers to todays AnyObject.

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