Optional promotion and empty optional chains

It is obvious that an empty optional chain has no effect, and we have an error diagnostic for it:

Optional(5)?
// error: optional chain has no effect, expression already produces 'Int?'

Furthermore, it is obvious that an optional chain on a non-optional value is pointless. You can just call methods directly without the optional chain. And we have an error diagnostic for that as well:

5?.magnitude
// error: cannot use optional chaining on non-optional value of type 'Int'

But, as is so often the case, two obvious things combine to make something non-obvious: an empty optional chain on a non-optional value would be useful.

• • •

Specifically, a postfix question-mark, interpreted in the natural way, wraps its non-optional argument in an optional:

let y = x?

First, the optional chain needs an optional value, so x is promoted to Optional just as it would be anywhere else.

Then, the empty optional chain is evaluated, which has no effect and simply returns its input. So this code—if it were permitted—would be equivalent to:

let y = x as Optional

• • •

It seems natural to me that “x?” should work, since we already have both optional promotion and optional chaining. These features ought to compose, with the effect of casting x to Optional.

We could raise a warning if a non-empty optional chain is called on a non-optional value, but the simple case of an empty chain should work without issue.

Pragmatically, this would make it easy to write optional values, and provide symmetry with the shorthand spelling for optional types. In one sense, it is syntactic sugar. But in another sense, it increases consistency by lifting an artificial restriction on existing features.

What do you think?

2 Likes

That doesn't seem natural at all to me. Optional chaining means to me "unwrap if possible and access the indicated member" (indeed, it is offered as an alternative to force unwrapping) but here you want to use the same postfix question mark to wrap. I'm not sure I see what artificial restriction is being lifted.

2 Likes

Perhaps I did not describe the situation as clearly as I could have.

Swift has an existing feature called “optional promotion”. This means that, if a non-optional value appears in a location where an Optional is expected, that value is promoted to (aka. “wrapped in”) an optional.

This is the existing language behavior, and we can demonstrate it with a simple operator:

prefix operator ^-^

prefix func ^-^ (x: Int?) {
  switch x {
  case .some: print("Something")
  case .none: print("Nothing")
  }
}

let n: Int = 7
^-^n      // "Something"

Here we take n, which a non-optional Int, and use it as the argument to an operator which expects an optional Int. This is only possible because n is automatically promoted from Int to Int?. We call this behavior “optional promotion”, and it exists because it is widely useful.

Thus, it is unexpected, surprising, and inconsistent with the rest of the language that optional chaining—which expects an Optional—does *not* utilize optional promotion. The compiler currently prohibits the use of a non-optional at the start of an optional chain.

Throughout the rest of the language, a non-optional can generally be used anywhere that an optional is expected, because it will be promoted to Optional as needed. But this is not the case for optional chaining.

I am suggesting that we make optional chaining more consistent with the rest of the language, by allowing optional promotion at the start of an optional chain, just as we allow optional promotion in other situations.

• • •

I hope that this detailed explanation helps to convey what I am trying to communicate. Namely, that the postfix question-mark would behave *more consistently* with the rest of the language (and with other operators that accept an optional) if we make this change.

Currently, optional chaining is *inconsistent*, because it prevents optional promotion. We could lift that restriction, and thus make optional chaining more consistent with the rest of the language in allowing optional promotion.

One natural consequence of doing so, is that an empty optional chain would have the practical and beneficial effect of allowing developers to write “x?” instead of “x as Optional”. The reason this works, is that optional promotion would apply in the same way as it does everywhere else.

Specifically, optional promotion would lift x to Optional before the optional chain is evaluated, because that is the only possible way in which the code makes sense. Then, the optional chain would be evaluated as normal. It takes an optional in, it performs the operations after the chain, and returns an optional.

When the optional chain is empty, that means it takes in an optional, performs no operations, and returns the result, which is trivially and obviously the same optional it started with. The optional chain operator—ie. the postfix question-mark—does exactly what it is supposed to.

It takes in an optional and returns an optional. That’s it. The fact that we can’t currently make it perform no operations in between, is an inconsistency. The fact that we can’t currently pass in a non-optional and let the compiler promote it to an optional, is an inconsistency.

The current behavior of optional chaining is inconsistent. I am saying we should fix those inconsistencies, and one practical benefit of doing so is that a drastically simpler way to spell optional values arises naturally from the standard behavior of optional promotion that exists in the language already.

1 Like

Optional promotion is a hardcoded subset of value subtyping; I think of it like optional covariance in that it's something we have in the language for now that ideally will be subsumed into a more general feature we just haven't had time to design and implement quite yet.

Optional chaining is an orthogonal convenience feature which allows users to use optional values in certain situations without explicitly unwrapping them.

The thing about convenience features is that convenience derives as much, if not more, from what they don't do; a convenience feature that is generalized to do everything ceases to become convenient.

The postfix ? operator serves as a compile-time assertion that the operand's type has at least one level of optionality. This is not an inconsistency to be fixed; in fact, it is essential to the design. Mixing optional promotion with optional chaining will allow ? to become a suffix for any value, erasing the diagnostics that can tell users that what they thought was optional (and therefore required optional chaining) actually isn't.

2 Likes

Right, these are orthogonal features, and they should behave orthogonally. Hence, they should compose with each other.

And as you say, optional promotion is indeed a case of value subtyping. So it is highly irregular that there are places in the language where the subtype cannot be substituted for the supertype. That should be fixed for its own sake.

Yes, as I mentioned, we can still raise a warning if a non-empty optional chain is called on a non-optional value. That way the compiler will still produce a diagnostic in that situation.

And to your last point, allowing ? to be a suffix for any value is one of the major benefits of what I am proposing here. The ability to write an optional value with syntax that parallels how one writes optional types is a significant win for both consistency and convenience.

2 Likes

No, in general, it does not make sense for features meant to check optional values to allow optional promotion in their operand. We don't allow it for ??, we don't allow it for if let, we don't allow it for !, and we don't allow it for ?.

7 Likes

The entire point of this thread is that this seemingly-obvious statement—which we have all been taking for granted—is in fact untrue.

It actually does make sense, and it is useful, to allow optional promotion in the operand of an optional chain.

• • •

Let me come at this from another angle:

• To make a value of type [Int], we can write [1]
• To make a value of type [Int: Int], we can write [1: 1]
• To make a value of type Int?, we currently *cannot* write 1?

When I brought this up in the past, I suggested adding a postfix ? operator that wraps its argument in an Optional. This would allow us to eliminate implicit optional promotion entirely, because it would be sufficiently convenient to write “x?” when needed.

The problem there is, such an optional-wrapping operator would conflict with the syntax for optional chaining, since postfix operators are evaluated before method chaining.

• • •

I left the issue aside and didn’t think much about it, until recently I realized that it does not have to conflict with optional chaining, because it *is* optional chaining. We don’t need a separate postfix ? operator, because the existing optional-chaining ? is good enough.

If we allowed an empty optional chain on a non-optional value, then we could write “1?” to produce a value of type Int?.

This is the underlying motivation here, to simplify the production of optional values. The fact that it can be done with a straightforward composition of existing features was a surprise to me, but it should work nicely.

But that's not an extension of the current behavior of the operator; it's a new and inconsistent special case. When people see x?, they expect that x is optional; they don't think of it as a conversion to optional.

You also haven't really explained why making a value optional is important enough to add really compact syntax for it. In most circumstances where it's needed, this happens implicitly.

4 Likes

Perhaps my communication skills have declined precipitously, because I believe I made a lengthy post explaining exactly how this works. The ? operator requires an Optional, so the existing optional promotion feature lifts x to Optional just as it would elsewhere. Then the empty chain performs no operation, and returns its input.

We have an abundance of existing features that make it easy and convenient to work with optionals. One missing piece is that we do not have a shorthand syntax for creating Optional values.

• To make an array, one can write “var a = [1]”
• To make a dictionary, one can write “var a = [1: 1]”
• To make an optional, one should be able to write “var a = 1?”

The ? operator requires an Optional , so the existing optional promotion feature lifts x to Optional just as it would elsewhere. Then the empty chain performs no operation, and returns its input.

Yes, and that is adding a new special case to the ? operator, which does not otherwise allow optional promotion. This complicates the operator and hurts readability.

One missing piece is that we do not have a shorthand syntax for creating Optional values.

Implicit optional promotion covers almost every use case for this. I don't think that's worth adding language sugar for.

3 Likes

It's also worth noting that if x is an array, [x] makes an array-of-arrays, but if x is already optional, x? would not make an optional-of-optional if it's supposed to be an extension of optional chaining.

2 Likes

Conversely, the general rule is “Non-optionals can be implicitly promoted when used where an optional is required.”

So it is in fact the places where optional promotion *doesn’t* work, that are the special cases.

Removing this special case from ? will simplify the operator and enhance readability.

Okay, this is a valid point of discussion. Most of the situations where I have wanted this feature involve initializing a variable, as “var n = 1?”. I think the shorthand is sufficiently useful for this purpose that we should include it.

Occasionally it might also be used to pass, say “x?” to a generic function (or, I suppose, an overloaded function) which could accept either an optional or a non-optional, and we want to pass an optional. This is a much rarer niche case, and I only mention it for completeness. It is not the primary motivation.

Excellent point. If we want, say, “1??” to produce an Int??, then we would need a separate feature for that.

I am not proposing such an addition at this time, as I think the simple case of making non-optionals into single-optionals covers the vast majority of uses.

No, no. I'm talking about

let x: Int? = nil
let y = x?

If x? is syntactic sugar for .some(x), then y would have type Int??. But if x? is equivalent to x?.somePropertyReturningSelf, then y would have type Int?.

(Aside: IIRC x?.self is currently rejected because Int?.self means Optional<Int>.self, which is a more important use case even if that's a little inconsistent.)

4 Likes

What I am saying--and I think what @John_McCall is saying too--is that this is not at all intended to be the general rule. In fact, features that are specifically designed for optional values (if let, !, ?, etc.) specifically check for them and prohibit non-optional ones.

4 Likes

Exactly. And this is not surprising to anyone — in fact, the reverse would be quite surprising.

Optional promotion also doesn't happen in the base of a member or subscript access.

2 Likes

I am thrilled and delighted to see that you have finally come around to agree with my longstanding view that rigid consistency of the language is not inherently beneficial.

After all, the straightforward and consistent answer is to say that T is a subtype of T?, therefore a value of type T can always be promoted to T? when necessary.

Regardless of whether we go that route, and regardless of whether we introduce a shorthand spelling for optional values, I hope that you will continue to share this understanding that consistency is not universally desirable in every situation.

That's not what I'm saying at all: there's no inconsistency here.

To be abundantly clear for everyone reading:

  1. “T is a subtype of T?, and values of type T are promoted to T? wherever necessary.”

  2. “T is almost a subtype of T?, and values of type T are promoted to T? in most, but not all, places.”

The first statement describes a consistent rule. The second statement describes an inconsistent rule.

Swift currently uses the second, inconsistent, rule.

I don't think you are correct in your conclusion. You seem to be applying your wherever necessary as wherever possible, and then concluding that Swift is being inconsistent.

It is one thing to argue that the set of wherever necessary could be expanded, but it's another to argue that Swift is currently inconsistent because you disagree with the set of situations the language currently considers necessary.

“Wherever a value of type T? is needed.”

This is the Liskov substitution principle.

It is one thing to argue that the current behavior is desirable and should not be changed, but it’s another to argue that a rule with multiple exceptions is consistent.