Swift Delight: Optional Conveniences
Swift delight: Optional conveniences
— Jordan Rose (@UINT_MIN) December 26, 2021
Lots of languages have something like Optional, but it’s so commonly used that it’s worth adding dedicated features for. I’m going to run through a few of them here.
Part of the Swift Regrets series.
Lots of languages have something like Optional, but it’s so commonly used that it’s worth adding dedicated features for. I’m going to run through a few of them here.
(Credit to Dave Zarzycki for reminding me that this is worth talking about. These weren’t on my original list because none of them originated with Swift, but they are relevant for someone designing a language with proper Optionals or even plain nullable types.)
First is sugar for the type itself, Foo?
for Optional<Foo>
. There are very very few downsides of this in my mind. Everyone uses it and it’s easy to understand, just like arrays. I think C# did this first, and it’s also in Kotlin; I’m surprised it’s not in more places.
Next is optional chaining, foo?.bar
(equivalent to foo.flatMap { $0.bar }
). This was an attempt to tame a feature both dangerous and beloved in Objective-C: calling a method on nil
returns nil
by default. (Or 0, or false, or…) In Swift we wanted to keep that basic capability but make it explicit, and it worked very well. In particular, flattening here rather than nesting Optionals (if bar
itself is Optional) was done to match Objective-C, but it extremely feels like the right behavior in practice. (See also the discussion of try?
from last time.)
But again, this isn’t original to Swift. Wikipedia says Groovy, of all languages, was the first with this operation, and it’s been in many since.
The other operator associated with Optionals is nil coalescing, foo ?? bar
. In Swift this can be used with two Optionals or with an Optional and an unwrapped value, and the second expression is lazily evaluated, similar to the boolean ||
. And bringing up ||
is not a coincidence! In a language where if foo
tests if foo
is non-nil, it makes sense for ||
to mean “the first value if it’s ‘truthy’; otherwise the second value”. A lot of dynamically-typed languages work this way, all the way back to LISP. But Swift didn’t go that route, and one of the main reasons was to avoid confusion around Optional<Bool>
. So we added a new operator instead. (Early versions of Swift did make Optional “truthy” and we took it out. if let
superseded the most common use anyway.)
The idea of this operator has been around a long time, often spelled foo ?: bar
because it’s similar to foo ? foo : bar
when the result is a C-style pointer or Java-style reference. We went with ??
instead because foo != nil ? foo : bar
is (potentially) missing an unwrap, and it also means all the “standard” operators with lazy-evaluated right-hand arguments are doubled (&&
, ||
, ??
…though not <<
or ==
). The only other language that spells this operation ??
is C#, but the delight is just having it in some form where I don’t have to use a method and explicit closure.
Finally, controversially, implicit conversion from Foo
to Optional<Foo>
. This is a big one, and I don’t recommend it lightly. Implicit conversions affect your type system and how people think about your type system. Can you convert (Foo?) -> Void
to (Foo) -> Void
as well?
For a very concrete example, consider ??
from before. Is line A equivalent to line B or line C?
let x: Int? = optInt ?? 0 // A
let x: Int? = optInt ?? Some(0) // B
let x: Int? = Some(optInt ?? 0) // C
A human can see that these are equivalent, but the compiler cannot, and it can’t decide by itself which is better. They both involve one function call and one implicit conversion! Because of this Swift has a special case for ??
to break the tie, but if a user writes equivalent overloaded functions they’ll have the same problem. (Answer: no type-based overloading?)
So I don’t recommend this lightly. But on the other hand, it absolutely makes Optionals nicer to work with. If you call a method with Optional params and you’ve got non-Optional values to pass in, of course you want to wrap them in .some
. There’s no ambiguity and no danger; why should you have to spell it out?
In general there are two kinds of optionality these days: true enum/ADT optional (Haskell, Rust, Swift) and nullability (C, Java, C#, Kotlin, JavaScript/TypeScript). The former languages tend to make you write .some(foo)
and the latter do not. Swift is the exception, partly for familiarity with Objective-C but also because it really does feel friendlier. So if you’re going to make Optional special in other ways, consider this one as well, even if only in limited contexts.
P.S. There is another Optional-related operator: forced unwrap (postfix !
). I said my opinion about this last time: it’s for cases where the inputs to an operation guarantee success but the compiler can’t know that, like URL(string:)
on a literal. It’s also good for scripts and tests and playgrounds, where you don’t need layers of error handling. It’s nice to have a concise spelling for this; Rust’s unwrap()
feels like the same sort of extra ceremony as Some(…)
. But since the whole point is that the compiler can’t check how you’re using it, I see how a longer spelling might be warranted. And even where I do use it, I wouldn’t call it a “delight”.
P.P.S. Why isn’t if let
on this list? Because I personally would have been fine if if let
were shorthand for if case let
, like it is in Rust: if let foo? = maybeFoo
. But our early users didn’t like it, so *shrug*
P.P.P.S. If there’s a lesson here for me it’s that I should’ve paid more attention to C# early on. Not being a Windows programmer I didn’t have much reason to get into it, but these days it’s my point of comparison for Swift, “an apps language building on what came before”.