We probably could make it work, but I would oppose it because having a postfix operator bind with lower precedence than an infix operator would be very confusing and I don't believe there's any other place in the language where we do that.
Given that we're currently parsing some P & Q as some (P & Q) (not to mention some P? as some (P?), the very issue under discussion), some is already behaving like a prefix operator with lower precedence than an infix operator. Not sure it makes the precedence rules any more wonky to have postfix operators bind with lower precedence in the mix too...
(We've (always?) had hardcoded, internally contradictory precedence rules with try hoisting, and I feel like this problem is of-a-kind here in that you're basically proposing some form of some (anti-)hoisting (?/!-hoisting?).)
Sorry, missed it somehow. I get the operator precedence argument, but if we agree that:
some P? unambiguously means (some P)?
some (P?) is invalid
Then there isn't any ambiguity in writing some P & Q?, as the only alternative interpretation would be some P & (Q?), which we know by (2) to be invalid.
Perhaps it's because I don't consciously think about operator precedence when reading code, but I wouldn't be any more confused about some P & Q? than over some some P?. All my brain sees is "some + [protocol thing] + ?", and I've already accepted that to mean "an optional value of some protocol" due to some P?.
Yeah, that's true. I would argue that the mandatory whitespace after the some vs. the mandatory lack of whitespace before the ? is what makes this potentially more confusing, though. Rather than toy examples, we should weigh whether something like some Protocol & OtherProtocol & SurpriseIHaveASneakyQuestionMark? would be legible enough.
As for the similarity with try, I'm not sure that generalizes to this because the important thing about try is just that it precedes some part of the expression that's throwing: for a non-throwing function f and a throwing function g, try f(g()) is identical to f(try g()), whereas some doesn't have that kind of distributed property.
But if it made the parsing rules easier to implement, maybe I could be convinced. Programmers are lazy efficient, after all.
There's a possible future extension to the any keyword that wouldn't be compatible: for any Array<P> to mean any<T: P> Array<T>, and any P? to mean any<T: P> Optional<T>. Maybe Optional<any P> and any<T: P> Optional<T> would have different performance characteristics or ergonomics. A significant difference is that any<T: P> Optional<T> would need a concrete wrapped type even in the nil case (but Optional<Never> could be a good default for the nil case).
That is simultaneously completely true and not an argument that it wouldn't still be pragmatically better for Swift programmers if we interpreted any P? as Optional<any P> rather than any<T:P> Optional<T>. Presumably a world in which we support such generalized existential type heads is a world in which the latter syntax, or at least any (P?), still exists if it's really what someone wants to express.
I don't think we'd want to "default" the erased type if we had generalized existentials.
I realize that there is not an alternative way of parsing some P & Q? that is valid in Swift, but I feel that parsing it as (some P & Q)? would be confusing, rather than expected.
Consider
let c: () -> Int? = { nil }
let d: (() -> Int)? = nil
Each of these is valid, and they cannot be conflated. c cannot be assigned nil, and d cannot return nil. I feel that rewriting some P & Q? to read as (some P & Q)? would be inconsistent with the way the closure signatures are parsed.
Again, I fully appreciate that the compiler will never conflate these scenarios, but I would seriously consider how inconsistent the binding precedence would appear to be.
Real-world examples can be make cases for both sides of the argument. What alternative interpretation would there be for a some Hashable & Equatable & Sendable?, more so in a world where some P? is routinely used as shorthand for Optional<some P>?
(I agree, however, that the trailing "?" without parenthesis has an impact on readability. But it's hard to tell how much is due to familiarity bias).
Yeah, I donāt think parsing postfix ? as binding more loosely than prefix keywords really works. Itās just too disconnected with how people will naturally interpret what they read. Better to say it really parses more tightly but that weāre choosing to give it a more practically useful interpretation instead.
Just to clarify, is this an argument in favor of an implementation that leaves the syntax tree and TypeReprs alone (producing Opaque(Optional(P)) for some P?) and having the type resolver to normalize it?
A couple thoughts (that are perhaps orthogonal to the proposal):
In SwiftSyntax, I hope we can provide a way to ācanonicalizeā variations like this (Iām not aware that this exists to much extent?). As we add more ways to spell things, it gets increasingly difficult to write macros that work across all the creative spellings people can do.
Can we make this form the default for generated Swift interfaces? It makes much prettier output for devs to consume.
As the guy who intuitively starts with any P? and then gets a compiler error, I approve of this proposal.
In all seriousness, I understand that this is not a "big problem": adding the parenthesis does not take a lot of time. And over time, I might learn to add them proactively. Yet, I also think it does something to help readability.
For me reading any P? left to right, intuitively means "any optional the conforms to protocol P". Whereas when I see (any P)? I need more time to "parse" because I need to "process" the parentheses first in my head. It's like it breaks the fluidity if reading from left to right.
Thanks for the feedback so far, folks. I've updated the implementation to handle the new syntax in the parser instead of in the type checker and updated the pitch PR to reflect that.
The pitch now also specifically calls out the some/any P & Q? case and suggests that we diagnose it as confusing; I still think this is the safer choice for now. It can be revisited in the future if practical experience suggests that we should.
The other possibility Iām assuming you want to allow is a parameterized protocol type, like any Sequence<Int>?. Itās worth mentioning explicitly in the proposal, but I donāt think it introduces any new parsing difficulties.
// Swift 6.2.3
protocol P {}
struct S: P {}
let existentialMetatype1: any P.Type = S.self // OK.
let existentialMetatype2: any P.Type? = S.self // Syntax error.
let existentialMetatype3: (any P.Type)? = S.self // OK.
let protocolMetatype4: (any P).Type = (any P).self // OK.
let protocolMetatype5: (any P).Type? = (any P).self // OK.
let opaqueMetatype6: some P.Type = S.self // Syntax error.
let opaqueMetatype7: some P.Type? = S.self // Syntax error.
let opaqueMetatype8: (some P).Type = S.self // OK.
let opaqueMetatype9: (some P).Type? = S.self // OK.