[Pitch] Allow `some P?` and `any P?` (removing the need for parentheses)

Let us unshackle ourselves from the parentheses that bind us. This is a fairly small syntax improvement, but one which frustrates me a great deal as an ExistentialAny user.

Link to the full pitch, which is copied below.


Improved Syntax for Optionals of Opaque and Existential Types

Summary of changes

Allows the types some P? and any P? to be written, having the same meaning as (some P)? and (any P)? respectively.

Motivation

Swift supports opaque types written some P and explicit existential types written any P, for some protocol P. However, due to parsing precedence, combining these types with optional type sugar (? and its implicitly unwrapped version !) currently requires wrapping the type in parentheses, such as (some P)? or (any P)?.

Developers frequently attempt to write some P? or any P? due to familiarity with standard optional syntax (e.g., Int?, String?). These forms are currently rejected, because they are interpreted as some (P?) and any (P?). The constraint following some or any must be a protocol, which P? (that is, Optional<P>), is not.

It is unlikely that a future version of Swift would ever define some/any P? to have a meaning other than what users would intuitively expect: an Optional of an opaque/existential type that conforms to P. However, a future version of Swift may require explicit any for existential types, meaning that there would likely be a proliferation of parentheses wherever optional existentials are used. Therefore, this simpler syntax improves ergonomics and readability.

Proposed solution

We propose to make some P? equivalent to (some P)? and any P? equivalent to (any P)?.

This extends to multiple levels of optionality. For example, some/any P?? will be equivalent to (some/any P)??, for any optionality depth.

Likewise, implicitly unwrapped optionals receive the same treatment wherever they are already supported. This proposal would make any P! equivalent to (any P)!.

Detailed design

Parser

This proposal does not change the parsing rules of the language. some P? will still be parsed as it is today, producing a TypeRepr with the shape OpaqueResultTypeRepr(OptionalTypeRepr(P)) (and likewise for any P?). TypeReprs are defined as the "[r]epresentation of a type as written in source", and we wish to continue honoring that definition.

Type Checking

When the type checker resolves an opaque or existential type whose constraint is an optional or implicitly unwrapped optional type, it will look through the type sugar to find the actual protocol constraint. As it does so, it will remember the depth of optionality so that the resolved type can be rewrapped correctly.

Special care is needed during name lookup when handling some types in function parameter positions. These use a slightly different code path because they are hoisted into the function's generic signature as anonymous type parameters. That is, we need to ensure that f(_: some P?) is treated as f<T: P>(_: T?) and not the nonsensical f<T: P?>(_: T).

Fix-its

Compiler fix-its for explicit existential any no longer insert parentheses when the existential type is optional or implicitly unwrapped optional. For example, when the following code is compiled with -enable-experimental-feature ExistentialAny today:

let x: P?

it generates the following diagnostic and fix-it:

warning: use of protocol 'P' as a type must be written 'any P'; this will be an error in a future Swift language mode
    let x: P?
           ^~~~~~~
           (any P)

This proposal changes the fix-it replacement to be any P rather than (any P).

Source compatibility

This is a purely additive change to the valid syntax of the language. Code that previously failed to compile (e.g., some P?) will now compile with the expected semantics.

Existing code that explicitly uses parentheses (e.g., (some P)?) remains valid and semantically identical.

Effect on ABI stability

This proposal has no effect on ABI stability. The types some/any P? and (some/any P)?—and thus their manglings—are identical.

Effect on API resilience

This proposal has no effect on API resilience.

Alternatives considered

Do nothing: We could continue requiring parentheses. However, this remains a common source of frustration for users adopting some and any.

Acknowledgments

Thanks to the Swift team for their prior work on opaque and existential types.

55 Likes

This is long overdue and I can see no drawbacks. I continue to believe, as I did in days of yore, that readers are inevitably going to read this like they do standard prose, where "How are you?" means "(How are you)?" and not "How are (you?)". For that reason, I don't think it'd ever be viable—useful or not—to have some P? actually compile but mean some (P?) or really anything else other than (some P)?.

In the alternative, we can do as the Spanish Academy did to clarify — ¿Cómo estás? — ¿some P?.

21 Likes

Is the proposal purely for the sugared spelling with a question mark?

That is, some P? will work, but some Optional<P> will continue to be an error?

And similarly with a typealias:

protocol MyProtocol {}
typealias P = MyProtocol
typealias Q = MyProtocol?

func f1(x: some P?) {}
func f2(x: some Q) {}
func f3(x: some Q?) {}

Is it proposed that f1 will be valid, but both f2 and f3 will continue to be invalid?

2 Likes

Insofar as some (P?) would continue to be an error, no?

2 Likes

What effect could this have on protocol composition?

let x: any P? & Q?

would then be equivalent to:

let x: (any P & Q)?

?

Correct. The proposal is simply acknowledging that for a single protocol, it is an ergonomic improvement to treat postfix ? as though it binds to the entire some/any type even though syntactically it binds to the constraint.

No, this would still not be allowed, because P? & Q? in isolation is also invalid. Protocol compositions still need to be parenthesized to ensure that the meaning is clear. I can update the pitch to make sure this is clear.

6 Likes

So if we have:

protocol P {}
protocol Q {}
typealias R = P&Q

func f1(x: some R?) {}
func f2(x: some P&Q?) {}
func f3(x: some (P&Q)?) {}

Then f1 will be okay but f2 will not?

What about f3?

2 Likes
  • f1 would be valid.
  • f2 would still be invalid, since it's unclear because P & Q is equivalent to Q & P, which begs the question of where the ? should bind.
  • I think f3 should be accepted as equivalent to (some (P & Q))?, but I need to verify that against my current implementation.
8 Likes

The need to add parentheses is certainly annoying, so I'm in support of the intention, and ultimately probably of this specific proposal. But I would be interested to hear somewhat deeper justifications of why there could be no other meaning assigned to some P?. The following future syntax is conceivable right?

func count(of array: some Array) -> Int {
    array.count
}

and similarly:

func isNil(_ optional: some Optional) -> Bool {
    guard case .nil = optional else { return false }
    return true
}

But this future direction seems perfectly compatible with this proposal, because if it were extended further:

func count(of collection: some Optional<Collection>) -> Int

this would desugar to:

func count(of collection: Optional<some Collection>) -> Int

which is precisely what this proposal would have:

func count(of collection: some Collection?) -> Int

desugar to.

But I would appreciate seeing a little bit of more explicit exploration of conceivable future syntax that demonstrates that this proposed syntax sugar doesn't close any doors.

5 Likes

some versus any is already a challenging point for learners without further overloading the meaning of some. So I don't think this is a conceivable direction, and I'd regard it as a plus if this pitch helps to further foreclose that conception.

4 Likes

I guess you could consider this "overloading" the keyword, but I think that from a beginner's perspective this usage would likely fall squarely within their understanding of how some should work. I think a beginner's initial understanding will likely be something like:

"If a type-like thing leaves some holes and therefore is sort of an umbrella over many possible fully concrete types, you can select the whole group under that umbrella by writing some Thing."

From an English perspective some Array reads perfectly.

6 Likes

Right, I agree that it's a plausible beginner's approximation for what some means. And for that reason, I think stringing them along by making a totally different feature share the same sugar, reinforcing the part of that approximation that's actually off the mark, hurts rather than helps a beginning user move from that (mis)understanding to a more accurate understanding of generics, protocols, and opaque types—i.e., it'd be "anti-progressive" disclosure, if you will.

4 Likes

But the only reason this is a "misunderstanding" is because some Array is currently disallowed, no?

No, it's a misunderstanding because generic types aren't opaque types.

1 Like

We've been talking about some in parameter position, so we are talking about generic types, not opaque types. The overloading of some for both concepts is already part of the language

3 Likes

They are actually the same concept; opaque return types can be thought of as part of the generic signature that are defined by the callee, rather than the caller.

func f(_: some P) -> some Q {}

is conceptually identical to

func f<T: P>(_: T) -> <U: Q> U {}

but for the fact that the latter syntax isn't valid in Swift today. However, in both cases, they represent constraints over a protocol or protocol composition, not a substitution that applies to arbitrary components of a concrete generic type.

3 Likes

You're conflating things here: f(_: some P) -> some Q is notionally shorthand for f<T: P>(_: T) -> <U: Q> U, where the caller chooses the specific T and the callee the specific U. Writing angle brackets after -> is the notional desugaring of opaque types that we don't have in current Swift, but it is the reason why people have referred to opaque types as "reverse generics."

In neither position does some X ever mean <each T> X<repeat each T> in addition to meaning <T: X> T—having one sugared notation which could mean one or the other depending on how X is declared would not be the solution to users not understanding the difference between them.

[Edit: @allevato types faster than me, and I see we've somehow written out the same toy example...]


By the way, in parameter position, the already-supported way to write what you want this to mean is [some Any]. And this pitch is what will allow users to write some Any? rather than (some Any)? as sugar for <T> Optional<T>.

2 Likes

I'm aware of most or all of the things that you both have written, but some things are not entirely clear to me.

By "arbitrary" are you referring to that the some Array syntax declares arbitrarily many implicit type parameters, since a generic type can, in general, have multiple parameters? Whereas at the moment some only ever declares a single implicit type parameter? This ability to declare multiple implicit type parameters with a single some is a larger divergence from the current usage of some than I had originally been thinking, but still, is it really a consequential "misunderstanding"? Is there any ambiguity at all about what some Array (or some Result) would mean if the spelling were allowed? It would just be convenient sugar for Result<some Any, some Any>, which is currently allowed and is quite a mouthful.

I know, and some Array reads much more naturally, don't you think?

There is already some precedent for this: extensions. I think it would be nice for some Thing to follow the same rules as extension Thing. Then, for example, defining a top-level function using some Thing would be similar to defining a method using extension Thing.

4 Likes

This seems odd to me, personally I think it makes more sense to express this as a syntactic rule rather than a semantic one. It does unfortunately mean you'll need to implement it in two places (the old and new parsers), but that seems better than needing to handle it in different bits of the type-checker that take TypeRepr inputs.

This would still be true, we'd just be changing the precedence of how parse the grammar.

2 Likes