SE-0335: Introduce existential `any`

And this is exactly why I have argued so strongly for Any<P>—because it at least resembles a distinct type.

1 Like

I asked above but did not get an answer.

Can we keep compilation and warning instead of error even in Swift 6 ? If no, I would be interested why, because the main issue with current proposal is it being a source breaking change, but if we keep it always as warning, it can be non-source break change.

I don't think it would make any more intuitive sense to say "Any<P> does not conform to P". What does Any<P> mean if not "any type that conforms to P"?

(To be clear, I know there are technical reasons behind this. I have read them before, understood them at the time, and then forgotten them pretty much exactly because they are not intuitive.)

Keeping the plain-protocol-as-existential syntax "deprecated" forever, as a warning, cuts off the future direction to re-purpose the plain protocol name to always imply a type parameter. I think this future direction is a very compelling reason to make this change, because the thing that folks write naturally would work, and they would be using the abstraction tools that we want them to use by default.

14 Likes

Thanks for the reply.

Current syntax does not allow future-proposed solution Sequence<Element>, would not it be still possible for compiler to distinguish those two different usages ( so that we can have "deprecated" and new behaviour together ) ?

I can shed a little light on the complaints/questions that I've seen very frequently as someone working on Swift's diagnostics.

This specific error message is a huge source of confusion, not only for beginners, but also for experienced Swift programmers:

protocol P { ... }

func generic<T: P>(value: T) { ... }

func testExistential(p: P) {
  generic(value: p) // error: Protocol 'P' as a type cannot conform to the protocol itself
}

This is a super contrived example, but people hit this error message all the time in a variety of ways, and the message doesn't match people's understanding of protocols. Their understanding is something along the lines of - when you use P as a type, you know you have a value that conforms to the protocol, so why does P not conform to the protocol?

There are potential language features that would allow the above example to compile -- by implicitly opening the existential to produce an opaque type, for example -- but there are other ways to hit this fundamental limitation that don't have a language feature to resolve it. Here's another example with the error message on main where SE-0309 is implemented:

import SwiftUI

struct MyView: View {
  var body: View { // error: Cannot infer 'Body' = 'View' because 'View' as a type cannot conform to protocols; did you mean to use an opaque result type?
    Text("")
  }
}

The programmer probably meant to use an opaque type here (which I why I recently added a fix-it to insert some). The only time you need an existential is when you want to conditionally return different underlying types in the implementation. To me, this is a strong indicator that some should be the default here, and programmers should opt into the dynamism provided by existential types only when they need to.

The family of errors that I anticipate becoming a big source of confusion after the acceptance of SE-0309 are the ones about API from the protocol not being available on the existential type due to associated types being erased. Here's an example, compiled with main where SE-0309 is implemented:

let collection: Collection = [1, 2, 3]

collection.index(after: collection.startIndex) // error: Member 'index' cannot be used on value of protocol type 'Collection'; use a generic constraint instead

I think this will really exacerbate people's confusion because it's very difficult to reason about what this error message is saying. When I use Collection as a type, I know that I have a value that conforms to Collection. This means that value has an implementation for all of the requirements on the Collection protocol. This API is a requirement, so I know that the underlying type must implement it. Why can't I call it???

I don't think this is just a problem with the error messages. Ignoring the fact that it requires several paragraphs to explain what's going on (which definitely can't fit in a single-line error message :slightly_smiling_face: ), when I explain this to folks, one of the most common responses I get is "wait, I thought I was using a generic constraint". And to be fair, that mental model isn't entirely wrong -- the protocol is a conformance requirement on the value that the existential type stores. But there is no indication at all that using the protocol "as a type" is any different from using it as a conformance requirement in a generic signature.

Obviously my experience only provides anecdotal evidence, but I've seen some pretty strong anecdotal evidence that the conflation between these concepts in the syntax has caused programmers to conflate these two concepts in their mental model, and it's caused a lot of frustration in the generic programming experience.

I believe that some Collection and any Collection will greatly help folks understand that there is a difference between constraints on opaque types and constraints on existential types, especially by giving us terminology to discuss these different concepts. "Collection as a type" has proven to be unhelpful, and using some kind of modifier in the terminology like "any Collection" doesn't make a lot of sense considering what you actually write in the source language to express "any Collection".

24 Likes

No, it would not be possible. Eventually, more sophisticated constraints like Sequence<Element> should be supported on both opaque and existential types, so one of those needs an explicit syntax. My argument is that the default should (eventually) be some, and any should be opt-in.

I find it hard to reconcile these three things:

  • Any<P> has been strongly opposed on the grounds of conflating generics and existentials

  • Sequence<Element> deliberately conflates generic and type constraint syntaxes

  • any P has been specifically cited as more supportive of the Sequence<Element> syntax

1 Like

That's why it's not proposed here. It (or rather the functionality that people want from it) is instead listed kinda as a future direction (even if it's not in that section of the proposal, which is a bit strange IMHO).
I think that this feature will instead probably turn out to be spelled somewhat like any Sequence<.Element: SomeProtocol>, but again, this is subject to further debate and not part of the current proposal at all. It's just another motivating reason (amongst many others) for going for any P instead of Any<P>.

2 Likes

In the future direction, Sequence<Element> is a generic constraint. This specific constraint was taken out of context in the comments in this thread, but in the future direction, this is written inside of an Array extension, where Element is a type parameter / opaque type. The constraint Sequence<Element> says that you have some type that conforms to Sequence where its element type is the same as the array's Element type. The angle brackets are sugar for a same-type constraint on the "primary" associated type of Sequence (which isn't a real feature yet, it's currently in the pitch stage).

With Any<P>, you're using a protocol as a "generic argument", which isn't actually a thing that's supported, and it would mean something different than Sequence<P> as outlined in the future direction. If someone wrote Sequence<P>, the P would be an implicit type parameter that conforms to P, and the underlying type would be known statically.

In this hypothetical future world, you can write any Sequence<Element> and some Sequence<Element>. The future direction suggests that we should consider allowing programmers to elide some instead of any. If we decide to never do that, all of my arguments against the Any<P> syntax still apply. I only brought up the future direction because never requiring any would prohibit us from explore this design space.

Right. This syntax for this feature is pitched over here: [Pitch] Light-weight same-type constraint syntax

1 Like

It also looks a lot better in documentation - which is important; the size and complexity of libraries we are able to build is limited in practice by our ability to explain what those libraries can do and how to use them. And a big part of that is casual browsing - glancing at a type's list of member functions and being able to quickly comprehend what's on offer.

Currently I'm seeing a lot of stuff like this, even in Apple's documentation:

init<C>(_: C)

Initializes an instance from a collection of <thing>

And there isn't really a great alternative today. But it'd look a lot better if in future we could write:

init(_: Collection)

Initializes an instance from a collection of <thing>

... which, currently, would be an existential.

It's one thing if newcomers can't understand the language (and it's reason enough to make the change), but it also means that libraries which use generics have a more awkward time explaining their interfaces.

It's not obviously related to the proposal, but it adds to the motivation for overhauling this whole area of the syntax.

7 Likes

A good chunk of the debate here centers around whether existentials are a separate, concrete, surface-visible box type of their own, or an implementation detail which is subsumed into the more abstract logic of the type system.

In other words, given this:

let x: any P = T()

…would we say that…

  1. the value x has a P (i.e. is a box that contains a T), or
  2. the value x is a P?

This has been a point of tension in the language, and even within the Swift team itself, at least since I filed this bug in 2016.

The Any<P> counterproposal would seem to lean toward codifying (1): existentials are a programmer-visible box type. The proposed any P syntax certainly leans toward (2), and perhaps I like the proposal because (2) is my preferred mental model. But (2) certainly does raise questions like the ones @jackmarch is asking — and I find myself hard-pressed to come up with answers to his questions that don’t lean at least a little on the “visible box” model of (1).

I really like the feature idea Holly mentions here:

I remember we discussed it here, and IIRC Joe Groff mentioned it at some point before that. I hope that idea will come up for consideration soon! It seems like it could help close the loop on making (2) a workable mental model.

(I should clarify that my +1 for this proposal is not contingent on the future adoption or non-adoption of that future direction.)

6 Likes

-1 for source breaking change,

From a language design perspective, it might make sense to make such change.

But, from a Swift users perspective, where I have to make lots of adjustment in my code and use my resources to do so ( even with fix-its, one has to update documentations, tutorials, samples ) change makes no sense, I don't see any benefit from it, there is no new functionality, just a syntactic de-sugar.

We don't even know that adding any prefix would fix the issue mentioned in the Motivation.
Why do we assume that people who misuse protocols today won't just add any in front and continue misusing it? Do we have any data to support it ?

For me, there is no justification for source breaking change that adds no new feature, requires investment from developers ( and authors ) and is based on gut feeling that the original issue mentioned in the Motivation would be fixed.

3 Likes

Obviously no one is going to change your mind, but you should consider the other motivation: to make different things look different. You can't even begin to explain the difference in a way people will grasp if they always look the same.

4 Likes

I've been a fan of the idea that any creates an existential box for the type it is applied to. Obviously some of the types are excluded from this, but it doesn't mean that it will remain like this forever. For example I think one door the any keyword will open is the potential in re-thinking some of the metatypes as there are some not so obvious holes in the language. Part of this would be the introduction of the true P.Type metatype and a generalized existential metatype for any type, or in other words any T.Type where T can represent any type.

That said, I strongly lean towards (1), especially when I think about the ability of extending or conforming existential box types to protocols.

extension any P: P { ... }
extension any P: Q { ... }

Would You mind telling me the relationship between SE-0335 and Generalized existential?
If we have the support with Generalized existential, do we need SE-0335 ?
Thanks a lot.

Is this still relevant? Reading the source code, that's not what I see. Am I looking in the wrong place?

Another recent example:

1 Like

This proposal is orthogonal to generalized existentials. This proposal adds an explicit keyword for writing existential types, but it does not allow you to express additional constraints on existential types. Similarly, generalized existentials would not give us an explicit, searchable keyword for writing existential types.

The technique remains relevant, even if the compatibility window for needing this for the specific marker protocol Sendable has passed.

Doug