Introduce Any<P> as a better way of writing existentials

The reason is that this is how the language currently behaves. Yes, it would be confusing for your proposed spelling of existential types, but I also would strongly object to treating angle brackets after Any differently from angle brackets after any other name.

My mantra here and elsewhere is that different things should be spelled differently and similar things should be spelled similarly. If the rules surrounding generic parameters are not a good fit for existential types, the solution is not to make an ad-hoc exception but to reconsider if spelling existential types as if they were generic Any is a good idea.

2 Likes

What magical properties? Prior to Swift 3, Any was literally just a type alias for protocol<>, the existential with zero protocol requirements. Are there other magical properties that have been introduced since then?

It would be special syntax. I'm not proposing a separate generic type distinct from Any; rather, I'm proposing an extension of Any

some P does not introduce a new wrapper type, though. It's effectively an "opened" existential, as I understand it.

There's also been discussion (in the same generics UI thread) about something like Collection<.Element == Int>. Do you also disagree with the use of angle brackets in that case?

Not really. In the LHS, T is a type. And any is used to make a type out of non-type entity - protocol or archetype. Keyword any cannot be applied to something which is already a type.

Unless we introduce some sort of kinds: typealias MyAny<T: Protocol> = any T

Further, my goal here is to make Swift more understandable for all users, in part by adding a clearer spelling for use in error messages. Adding MyAny<T> = any T doesn't produce clearer error messages at all.

1 Like

I disagree here, as you can theoretically wrap every possible instance of any type into an existential. The existential will have the behavior depending of the type it wraps. There is no need to limit this to protocol-as-a-type.

class A {}
class B: A {}

let a: A = B()
//     ^ what is this container? In my eyes, this is `any A` existential.

Theoretically you can write any Any and with the preferred syntax from the pitch author Any<Any>, but the latter could potentially confuse the compiler.

struct T {}

let t: any T = T() // theoretically okay, but useless for structs

To me, the most straightforward solution here is to allow Animal to be a concrete type conforming to itself when this wouldn't cause any problems, i.e. when it doesn't have static, init, Self, or associatedtype requirements (the latter two cases already disallowed). We could allow protocols to be valid types only when these aren't present, and require Any<P> otherwise.

I picked projects at random from the Source Compatibility Suite and looked at the protocols they defined; they mostly didn't use init or static requirements, and when they did, it's mostly in protocols that also use Self or associatedtype. It would be a shame to make a language feature more obscure for the sake of an uncommon use case.

1 Like

Absent some technical reason why this wouldn't work, I'm pretty much in agreement. We already have the one-off Error self-conformance and countless questions about this (unnecessary, IIUC) limitation, so it's not as if the utility of such a feature is in question. In my mind the biggest questions around this feature are:

  1. Does the additional diagnostic "protocols with init or static func requirements cannot self-conform" leave us in a worse place than the existing "protocols cannot conform to other protocols" diagnostic?
  2. Is such a self-conformance implicitly supplied simply by declaring the protocol?
  3. If not, how should the self-conformance be spelled in source?
  4. If we offer a way to spell self-conformance explicitly, should we extend this syntax to allow protocol types to conform to other 'simple' protocols?

In the interest of not derailing @bjhomer's thread, I suggest that this line of discussion be diverted to an alternate thread where the pros and cons of generalizing self-conformance can be discussed in-depth.

3 Likes

Separating the spelling of existential type from the spelling of protocols is important and good.

I'm thinking of existential types as variables of protocol type, where you have a name in source explicitly declared as something that conforms to a protocol, like var x : P . As opposed to protocol definitions, which look like protocol name {} That might be imprecise still but it helps me to be concrete.

I'm not an expert and have been generally wrestling with this mentally for a while. So some of this might reflect my own ignorance or partially-understood things.

A couple of notes:

  1. I don't know how to measure how much of the confusion around this is because of using the same spelling for the two things though. Opaque types with some are straightforward but took me some time to really understand. Some of the issue is that the terms opaque and existential have precise meanings, but the regular words opaque and existential also mean something related but different.

  2. @Nickolas_Pohilets already said this but I also agree that Any<P> looks like P is generic. I think people (me) are already confused some of the time about what's a generic and what's another kind of type. I tend to think that angle brackets should mean generic parameters, and everything else should have different syntax. That would be roughly consistent with the idea that protocols can't have angle brackets, only associatedtype, and so on. Using any T instead of Any<T> keeps that distinction.

  3. The word any means a lot of things in English. Does it make sense to explicitly say var anyAnimal: existential Animal instead of var anyAnimal: Any<Animal> or var anyAnimal: any Animal ? Using a more precise keyword might avoid some confusion. The some keyword for opaque types is not too bad but I did have to go back and read the documentation a few times and write a bunch of test code to be sure I understood it. If it had been opaque T instead of some T it might be clunkier at first but more precise once I understood it. I actually like some because it's shorter and easier but it's worth thinking about.

Just to underline, I think changing the syntax to separate protocols from existential types is really important no matter how you do it.

I think it would be better to lump init/static protocols in with Self/associatedtype protocols than to bifurcate protocols into a yet third invisibly differentiated kind with idiosyncratic type system interactions.

This is relevant to this thread because types for init/static protocols should be replaced with something (even before we get existentials for Self/associatedtype) so we need some new syntax for that. (Any<P> seems good to me.)

Maybe we could use something like:

var anyAnimal: existential<Animal>

I don't think we could use "Any" because using a identifier for a non-generic type and a generic type confuses the parser. I wouldn't object to something shorter than "existential." I want the brackets so more complex expressions for the protocol name won't spew punctuator soup into the rest of the source file unconstrained. Besides protocol compositions, we can use this for allowing existentials for types with associated types:

var literal: existential<ExpressibleByUnicodeScalarLiteral where UnicodeScalarLiteralType == Character>

(Note: all of the associated types have to be locked down, and there can't be any Self requirements.)

Could you explain how a different spelling, be it any Animal or Any<Animal> instead of Animal resolves this issue? I think the problem is fundamentally "this error is possibly stemming from a conceptual gap, and the concept is a difficult one to grasp, which makes it almost impossible to explain the problem in the limited confines of a diagnostic message". Also, this diagnostic was recently updated on master to be:

error: protocol 'Animal' as a type cannot conform to the protocol itself; only concrete types such as structs, enums and classes can conform to protocols

Not saying that this is perfect, but it does seem like an improvement.

We also have an educational note explaining this in more detail.

I think the problem is that many users don’t currently think of Animal as an existential, they think of it as a supertype. And why shouldn’t they? There’s a clear analogy between

protocol Animal { func noise() }
class Dog: Animal { func noise() { print("Woof") } }

and

class Animal { func noise() {} }
class Dog: Animal { override func noise() { print("Woof") } }

Overall I think this is a strength, not a weakness. Protocols look like supertypes and usually act like supertypes, so most of the time they should just be supertypes.

In the cases where they don’t act like supertypes, really they aren’t conceptually types at all, and a new syntax (Any<P>) would make this clearer.

4 Likes

I like the proposal, as I think that there’s great value to differentiate the existential from the protocol in how it’s spelled. I prefer any though.
Also, but it’s a separate proposal imho, existentials that can conform to their protocols should do so automatically (and the error message when they don’t should specify which are those restrictions)

1 Like

I'm overall in favor of segregating protocols from their existential types as proposed above. However I think some alterations would be integral. Thus, I am going to propose some more changes, which - to be clear - I do think are very hard to implement in the real world.

So what do I propose?

I think the proposed way to refer to a protocol existential types (by parameterizing Any) is quite good. The syntax is clear and easy to use - yet not too lightweight - so that people don't mindlessly resort to Existentials in cases where Generics would be better. However, this syntax also poses a lot of problems and has drawn criticism over its odd parameterization behavior:

let foo: Array<Int> 
    // Generics Syntax

let bar: Any<Error> 
    // Existentials Syntax

let baz: Any 
    // What happened here?
    // `Array<Element = Int>`
    // is currently invalid,
    // so what's this

So to tackle this problem, I propose that we add a supertype "Value" for what is currently "Any"- thus improving the parameterization behavior of the proposal. This way, we would separate the hypothetical supertype Value from the existential-related Any. Furthermore, potential future syntax - as discussed in Improving the UI of Generics - would be improved :

func foo(bar: some Value) {}

// equivalent to:

func foo<Value>(bar: Value) {}

Moreover, a clearer distinction between Generics, Existentials and the super-type Value would be established:

protocol Value {}
// Every type implicitly 
// conforms to `Value`.


let foo: Value
// Since `Value` would
// essentially be a protocol
// we cannot bound `foo` to 
// the `Value` itself, but rather
// its Existential.
❌ Cannot use `Value` as a type.
Did you mean `Any<Value>`?

Also, many have expressed their concerns about how parameterization would interact with other non-protocol types. Personally, I see that not as a constraint, but rather as a push towards generalized existentials which would support protocols as well structs/classes/enums:

let foo: Any<Array>

let foo: Any<Int>
⚠️ `Any<Int>` is equivalent to `Int`.

let foo: Any<Any<Array>>
// Since `Any<Int>` is a 
// concrete type, its 
// Existential would not serve
// any purpose. It's like the 
// above example.
⚠️ `Any<Any<Array>>` is equivalent to `Any<Array>`.

Why Not Use an "any" Modifier?

In my opinion, an "any" modifier (written as such: any Error) would be too lightweight to use for Existentials. That's because despite being great for certain situations, Generics are often a better choice. I'm concerned that by adopting the "lightweight" syntax many beginners would be unable to differentiate between the two and, as a result, instinctively choose the former over the latter. Not to mention, that Existential Types are just that: Types. That is, despite their boxing behavior and syntactic magic they'll still largely behave as Types in future language versions (where they'd be extensible).

I agree with this.

func takeASpecificAnimal<T: Animal>(_ animal: T) {}

To me, the most straightforward solution here is to allow Animal to be a concrete type conforming to itself when this wouldn't cause any problems, i.e. when it doesn't have static, init, Self, or associatedtype requirements.

I'm wondering if it would work to require the some modifier to get access to static/init/Self parts of a protocol like this:

func takeASpecificAnimal<T: some Animal>(_ animal: T) {
    // access to T.someStaticMethod()
}

func takeASpecificAnimal<T: Animal>(_ animal: T) {
    // no access to T.someStaticMethod()
}

Existentials could then be used to specialize all generic parameters that don't have the some modifier.

I would guess that requiring <T: some Animal> to access all the properties of T would be far too source-breaking to consider. My main goal in this pitch is to make it more clear to the user when they are using existentials; right now, they're mostly invisible. I'm less interested in pushing for a large-scale change to generics that's unlikely to go anywhere.

2 Likes

Just one more thing that came to my mind. What is the exact motivation of this proposal? If it‘s only a distinction between types and better (error) logs then I personally don‘t think that the high bar of such massive disruption would be met. We have to preserve source compatibility so var v: P should remain to work, but I also think and hope at some point we want to force the type to be more explicit, which would make it var v: any P.

Will the ability of extending existentials only be implied from this proposal?

I personally think that we would have to add at least that functionality to get somehow close to the high bar. In other words, what does the pitched solution enable the user to do? If the user can‘t create distinct extensions, then there is nothing added other than syntax disruption:

// extends the functionality of P
extension P {}

// extends only the existential
extension any P {}

// POTENTIALLY IN THE FURURE:
// extends only all conforming types of P
extension some P {}

And as I‘m writing this, we could finally manually express existential conformance to their protocols.

extension any P: P {}

If these are only long term goals, then I personally don‘t see this proposal to make through the review as it would only harm the user as the explicit distinction didn‘t also enable new features.

5 Likes

The motivation of this proposal is that I find myself frequently having to explain the difference between protocols and existentials, as well as how they interact with generics. I see it frequently in my conversations with local developers, and it also comes up here on the forums fairly often. It's difficult to even talk about the difference between a protocol and an existential when they have the exact same spelling, so it's not surprising that users don't understand it.

I'm not proposing adding existential extensions right now. Joe Groff's "Improving the UI of generics" post from last year anticipates the ability to extend the existential type itself, using the spelling extension any P. I find myself wishing that we had the any P notation now (even before we have the ability to extend existentials), simply for the increased clarity it provides. It takes a confusing part of the language and makes it less confusing. I believe that has merits on its own.

However, for reasons I've tried to enumerate in the original post, I think the any P spelling is confusing. My main objection is that the natural reading of the following suggests that it extends any conforming type:

extension any Animal {
	func speak() -> String { ... }
}

If that is truly an extension of any Animal as is the obvious reading, then it seems natural that you should be able to call speak() on any animal. And yet, I cannot call dog.speak() or cat.speak(); I can only call anyAnimal.speak(). So the anticipated syntax extension any Animal seems actively harmful to me; if people are confused about existentials now, I worry that using that proposed syntax will make them only more confused in the future. extension Any<P> suggests that it clearly extends only a single type, not all types.

I'm also concerned that it's easy to get lost in the soup with a declaration like this:

// This is difficult to follow (for me)
var x: any P & Q where P.X == Q.Z = SomeStruct()

// This is less confusing to me
var x: Any<P & Q where P.X == Q.Z> = SomeStruct()

The <> delimiters help my eye recognize where the type declaration ends and the initializer expression begins. Again, though, we don't have the ability to write such expressions at all right now, so this is not an immediate concern. I just worry that if we go down the any P road, it will lead to confusion.

I've used the explanation that existentials are basically an Any<P> multiple times when answering questions from fellow developers, and have found it to be consistently useful in helping people understand what's happening. My hope is that we can get the clarity of a better spelling now, and I've tried to design it in a way that makes it compatible with future features.

So in short, my motivation is to address a source of active confusion: developers not understanding that var v: P introduces a new existential type. I also hope that I can nudge the syntax toward what I feel is a better spelling (Any<P> instead of any P), but if the consensus is that any P is the better spelling, I can accept that. Whatever spelling we choose, I hope we can get the benefit of a clearer spelling without having to wait for additional features.

Because of the need for source compatibility, I don't think we can force everyone to convert var v: P to var v: any P anytime in the near future. There's so much existing code out there using existentials that I'm not sure we'd ever be able to do it. I know I wouldn't want to go through and convert all the code in my own code base. But I'd still like the benefit of clarity for new code.

10 Likes

Personally, I'd rather have this:

var x: (P & Q where P.X == Q.Z) = SomeStruct()

In short: just add a where clause to the existing syntax. I'm not thrilled at the idea adding an alternate syntax (any P or Any<P>) to express something that already has a syntax in the language.

4 Likes

One of the things I like about any P is that it could potentially be extended in the future to give a name to the "opened" existential:

let x: any Animal A = ...
///We can now use A to refer to the exact type opened...
Terms of Service

Privacy Policy

Cookie Policy