SE-0335: Introduce existential `any`

Hello Swift community,

The review of SE-0335 " Introduce existential any" begins now and runs through December 22, 2021.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager. When emailing the review manager directly, please keep the proposal link at the top of the message.

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available at

https://github.com/apple/swift-evolution/blob/master/process.md

Thank you,

Doug Gregor
Review Manager

48 Likes

I like it, +1. Always thought it’s not right that the best spelling is reserved to something that should be the exception, not the default.
Great work @hborla et al, clearly a lot of effort went into this but I think it’ll be worth it!

5 Likes

I think this is a timely and important proposal.

There has been disagreement on this point, but I am firmly of the opinion that the problem being addressed is significant enough to warrant a source-breaking change, staged in over language versions, for reasons already enumerated during the pitch stage (and for which Rust also earlier made an analogous transition, if I understand correctly).

The final shape of the proposal reflects thoughtful consideration of a number of issues which I believe have been resolved to the best possible degree within the limits of a single proposal. Namely, I agree with the proposed meaning of (any P).Type and its distinction from any P.Type, as well as the proposed handling of typealiases.

(It may be worth calling out in the extreme corner case where the bare protocol is currently typealiased both for protocol renaming purposes and for the purposes of fulfilling associated type requirements that this will no longer be possible in Swift 6, for the same (desirable) reason as the overarching thrust of the proposal that we are making an explicit distinction between the bare protocol and the corresponding existential type, much as we doubt that it is common to be making use of a typealias for such dual purposes.)

I am not a practicing Rustacean and therefore cannot speak to how this proposal compares to the transition that Rust undertook; some discussion from someone who can speak to that would no doubt be illuminating.

I took part in the pitch phase and have reviewed the present version of the document thoroughly.

16 Likes

+1. This is a hugely important improvement. Helping people out of the design corners they've accidentally wandered into with using existential types is likely the absolute majority of the time I've spent overall in helping people how to learn Swift.

5 Likes

+1 This looks great and I truly welcome being able to reason about when a type is existential or not like this :+1:

I have one question: what happens with marker protocols?

Like, the following examples all should error out since they erase down to Any so this ends up the same as any Any and any AnyObject respectively:

// @_marker protocol Sendable {} 
let s: any Sendable // should be error I think?

// @_marker protocol AnyActor: AnyObject {} 
let a: any AnyActor 

I'm assuming that's just how it works, but figured I'll double check.

--

  • I tracked the discussion and read the proposal thoroughly.
  • The problem is important to address :clap:
  • Other languages I'm familiar with don't suffer as much from existentials since they can optimize using JITs at runtime, so not much precedent form other languages I'm familar with, but this feels right at home in Swift :+1:
4 Likes

I think the proposal should state clearly (in the Proposed Solution section) about how programmers should write existentials for protocol compositions, and what if we combine a normal type with an existential type. I can see that in some code snippet we already have this:

let compositionMetatype: any (P & Q).Type = S.self

I assume we should write existential for protocol compositions as Any (P1 & P2 & P3), and if a normal type kicks in, we should write UIView & Any (P1 & P2 & P3). But these cases should be addressed explicitly in the Proposed Solution section of the proposal I think.

1 Like

The restrictions on what can be used as an existential type with any in this proposal aren't different than what can be used as an existential type today. I just double checked, and this code isn't banned on main:

let value: Sendable = 10 // okay
print(Sendable.self) // prints Any

I'd be a little nervous to make a "source breaking" change with the introduction of any on the off chance that someone has actually written code like this. That said, the implementation model clarifies existential types from protocols across the board, not just ones written with any, so it would be straightforward to diagnose a marker protocol used as an existential type with or without any.

4 Likes

Maybe I'm being dumb, but the proposal has this example:

// Swift 6 mode

protocol P {}
struct S: P {}

let p1: P = S() // error
let p2: any P = S() // okay

which is exactly (it is, right?):

// Swift 6 mode

@_marker protocol Sendable {}
struct S: Sendable {}

let p1: Sendable = S() // error
let p2: any Sendable = S() // okay

But that any Sendable is a bit weird, so I wanted to clarify what is supposed to happen there.

At runtime Sendable is exactly Any, so this any Sendable actually is like any Any...? ~And we're trying to ban any Any, so... what should happen here?~

Ah, we're not banning any Any we're just issuing warnings about it:

let value: any Any = S() // warning: 'any' is redundant on type 'Any'

so I guess the same should happen for the any Sendable then? :thinking:

2 Likes

I'm happy to add a code example, but the proposal does specify this in the grammar. Syntactically, any can be applied to another type*, which means that you can apply any to an entire protocol composition, e.g.

protocol P {}
class C {}

let value: any C & P = ...

*as far as the grammar is concerned, a protocol composition is a type, though I'd love to phase out the terminology "protocol/composition as a type" and instead refer to these as constraints.

Note that the syntax for any with protocol compositions is exactly the same as the syntax for some.

7 Likes

Thanks for the explanation. But I still think grammar belongs to the implementation detail of this proposal (the Proposed Solution section seems more like product specification). As an iOS engineer, I don't quite often look at the grammar rules to learn some language features. (this may not apply to other engineers though)

Why does the fact that Sendable is a marker protocol matter? Only values whose static type conforms to Sendable can be assigned to values of type any Sendable; therefore for all practical purposes any Sendable is a real type distinct from any Any.

2 Likes

That’s my question :slight_smile:

Understood; the explanation was to answer your question, and I am still happy to add a code example to the proposed solution section.

I agree; semantically, any Sendable and any Any are different. If marker protocols are usable as existential types -- which they are today, and this proposal does not change that -- they should require any. The reason why any is not required on Any is because it's redundant, not because any is inherently meaningless for unconstrained existential types.

9 Likes

That sounds good, thank you for clarifying!

I’m a strong +1 on this, both the large-scale spirit and the details. Just to bring some spe the approval chorus, I like the spelling change for three reasons:

  1. The “some” / “any” vocabulary provides a good casual mental model for a complex idea. I’m a strong believer in having good heuristics at the language surface: before that web search, before that careful study of the docs, syntax and vocabulary should lead to reasonable guesses about meaning. In the teaching half of my life, I see how strongly new developers lean on the surface features of languages for deep reasoning about them. Putting myself in the shoes of somebody learning about this stuff for the first time, “Why can’t I return x: P from f() -> some P?” is a really tough question, but “Why can’t I return x: any P from f() -> some P?” is a question that’s already half-answered. (I note that the rather mind-boggling error messages for that mistake could profit from this proposed terminology!)
  2. I remember @Chris_Lattner3 talking about the rationale behind using let instead of const: programmers impulsively reach for the easier thing, and retcon a rationale for it. People would type var because it’s shorter, then invent some reason why that’s Really the Best Choice. The spellings any P / some P honor this same principle in a way that the status quo does not: the path of least developer attention should not lead to existentials.
  3. I have a vague memory from past discussions that some of the more complex directions for generalizing existentials would profit syntactically from this spelling, particularly when constraints are involved. I know we are supposed to primarily evaluate whether proposals stand on their own terms, but it’s hard to ignore those future directions. Same for the func append(contentsOf newElements: Sequence<Element>) example in the proposal.

All of that says to me that yes, this is worth the source breakage. And I’m happy to see that judicious source breakage is still on the table!

9 Likes

This does bring up a somewhat related issue that I just want to make sure has been considered:

There have been recent occasions where code like the following has been needed (related to the new concurrency stuff, I think—sorry, the exact scenario is slipping my mind so this is just a sketch):

#if <some feature or language version check>
typealias _Foo = Fooable & Barable
#else
typealias _Foo = Any
#endif

Just want to make sure that even as the proposal will warn about redundant any Any that it won’t warn about “redundant” any _Foo regardless of language version, because it’d be impossible to address or silence that warning.

(And if having the warning only for any Any and not for any type aliases is not easily done, I would argue better not to bother warning at all than to warn spuriously.)

10 Likes

I participated in many threads related to existentials and the pitch thread of this proposal. Overall this is a great proposal and it does improve the language a lot as it will sort out many confusions the Swift community run into, but not only that, it will also open new doors for extensions and possibly new constraints in the generic world.

I appreciate that the proposal did revised the ideas behind metatypes. While I personally think that there is still a lot to discuss regarding metatypes, especially the existential metatype, I agree that it's not an issue of the initial proposal as it only takes the existing types and re-brands them in a way by statically and visibly generate a clearer difference between them.

If anyone wants to dive deeper into the abstract problems with metatypes, I kindly invite you into the pitch thread, because it would be unfortunate to re-hash all that has been said again in this review thread.

Anything that remains an error after this proposal, can still be potentially unlocked with a future proposal. ;)

+1 For me!

6 Likes

If you have a type alias to Any or AnyObject, do you have to use the any keyword to declare an existential?

typealias Foo = Any

let x: Foo = 5 // is this valid?
let y: any Foo = 5 // does this produce a warning about redundancy?

I think it should still be required since any Foo isn’t redundant and it would avoid the scenario that @xwu mentions above. However, that would mean that code like the following would be required to express a type alias for the existential form of Any.

typealias Bar = any Any

We have a recent example of this technique at guides/concurrency-adoption-guidelines.md at main · swift-server/guides · GitHub

2 Likes

Strong +1.

Yes, I fully agree with the proposal motivation. I think status quo has been a significant source of confusion with people not realizing what impact existentials have on code they write.

Overall, yes. It makes the notation explicit where it should've been explicit in the first place.

I'm aware of Rust taking a similar approach with trait existentials. I'm not aware of significant issues that approach have caused so far, so I think on balance it did work well for folks using Rust.

I followed the pitch thread and read the proposal.

1 Like