Optional chaining with operators

Optional chaining can be used with methods, mutating methods, and assignment operators:

var x: Int? = 0
x?.addingReportingOverflow(1)
x?.negate()
x? += 2

However, it currently cannot be used with non-assignment operators:

x? + 3  // error: Value of optional type 'Int?' must be unwrapped to a value of type 'Int'

This seems inconsistent. Is there a reason for it not working?

I don’t think it’s really an inconsistency. The question mark before accessing a property / calling a function implies that the ensuing access is optional, as in “ negate x (optionally)”. On the other hand, using such syntax with operators doesn’t seem that intuitive. The question-mark sugar could already be considered too confusing for beginners. Thus, visually separating the question mark for optional chaining (with a space character) from the declaration to be chained, comes with readability problems.

We already allow optional chaining before assignment operators.

The inconsistency is that this works:

x? += 1

But this does not:

let y = x? + 1

Or, to drive home the inconsistency even more directly:

x? += 1       // valid
x = x? + 1    // error
1 Like

Not to say that this is "the reason" why it doesn't work, but it would be pretty weird to have a? + b but not a + b?, since arithmetic operators generally treat both operands equally. If we were going to do something like this, I would probably prefer a spelling like a +? b or a ?+ b that could chain on either or both sides.

We already have that situation with unary operators: as described in the thread Optional chaining on prefix operators, optional chaining works with postfix operators x?... but not prefix operators ...x?.

I don’t think it’s surprising to say, “optional chaining takes an optional on the left and a function on the right”.

• • •

My actual use-case for this, besides a general sense that it ought to work for consistency, is dynamic casting:

(t as? T)?.foo()   // valid
(t as? T)? == u    // error

I’d like to be able to write the latter.

1 Like

I wrote this earlier today, and it builds and runs as expected:

if (error as? MockDataProvider.Error) != .seekError {

Also works if .seekError is stored in a variable.

That’s making use of optional promotion.

But if the dynamic cast is to Optional (or ExpressibleByNilLiteral), and the right-hand side is nil, then it compares at the top-level instead of at the wrapped-value level.

And of course, one might want a different operator besides ==. Maybe <, maybe +, maybe forming a range, or something else entirely. Optional promotion won’t let those work.

1 Like

While I see the apparent inconsistency at the call site, there is a deeper difference.

x + 3 is not a syntactic variant of x.+(3) — for some hypothetical instance method named +(_:) — but is rather +(x, 3) — for some actual global/static function whose name is +(_:_:).

So, I see the inconsistency as a reflection of the fact that optional chaining doesn't reach "into" function parameters, though that feature has been requested before , e.g. here and here.

The other difference is that there's no clear chaining in x? + 3, which is another way of saying what @scanon said. With a form like x?.plus(3) it's obvious where the "chain points" are in an expression. With x? + 3, it's not entirely clear which subexpressions are evaluated before or after the point where an overall nil result can be produced.

Assuming all of that could be sorted out syntactically and semantically, I think operator chaining would still be problematic, because it overloads ? use-cases so far as to potentially make ? seem like a magical DWIM operator.

It's not a terrible idea, I think, but I'd sure prefer if we could continue to manage without it. :slight_smile:

Nor is x += 3 a syntactic variant of x.+=(3), yet x? += 3 still works.

1 Like

Well, either the assignment does that as a result of some obscure relationship to a different syntax, or it's a magical DWIM case already.

As I said, I don't necessarily want to reject more magical DWIM, but my preference is to err in the other direction.

…so it turns out that users can already make this work, by defining new precedence groups that are copies of the existing ones but with assignment: true, and redeclaring the operators for that precedence.

For example, this:

precedencegroup MyAdditionPrecedence {
  assignment: true
  associativity: left
  higherThan: RangeFormationPrecedence
  lowerThan: MultiplicationPrecedence
}

infix operator + : MyAdditionPrecedence

Lets you write:

let x: Int? = 3
let y = x? + 4
print(y)   // Optional(7)

• • •

Does the assignment field of a precedence group do anything other than enable optional chaining for operators?

If not, then making this work seems fairly simple. We could do away with the assignment field entirely, and make all operators work with optional chaining. Or at least, we could make the default be to allow optional chaining.

According to The Swift Programming Language:

The assignment of a precedence group specifies the precedence of an operator when used in an operation that includes optional chaining. When set to true , an operator in the corresponding precedence group uses the same grouping rules during optional chaining as the assignment operators from the standard library. Otherwise, when set to false or omitted, operators in the precedence group follows the same optional chaining rules as operators that don’t perform assignment.

Yes, and I wrote a reply to you a year ago with the answer. Namely, try hoisting cannot be expressed in terms of Swift precedence but looks to whether the neighboring operator is an assignment operator.


Even if we didn't have any special rules for assignment operators, you still couldn't just allow optional chaining for non-assignment operators and expect things to go well. Recall that a chain implies that there are multiple "links":

struct Foo { var bar: Int? }
var foo: Foo? = Foo(bar: 42)

print(foo?.bar == nil)           // false
print(type(of: foo?.bar == nil)) // Bool
foo = nil
print(foo?.bar == nil)           // true

In other words, using parens to indicate order of operations, we have (foo?.bar) == nil.

Here's what happens when you allow optional chaining:

struct Foo { var bar: Int? }
var foo: Foo? = Foo(bar: 42)

precedencegroup _ComparisonPrecedence {
  assignment: true
  associativity: none
  lowerThan: NilCoalescingPrecedence
  higherThan: LogicalConjunctionPrecedence
}
infix operator == : _ComparisonPrecedence

print(foo?.bar == nil)           // Optional(false)
print(type(of: foo?.bar == nil)) // Optional<Bool>
foo = nil
print(foo?.bar == nil)           // nil

In other words, using parens to indicate order of operations, we have instead (notionally) foo?(.bar == nil). This is a totally different expression than the status quo shown above; users (or, at least, any user who's written any code already) actually wouldn't want this chaining for non-assignment operations.

5 Likes

Thanks @xwu, that explains it!