Nested Optionals in Swift, design mistake?
--
Some time ago I stumbled upon this extension:
Okay, so what’s going on here. At some point you will encounter nested optional type, for example, if you have a dictionary with optional value and trying to get value by the key:
So extension works well, but it led me to a logical question: “Why this is even a thing?”. And what I mean is that actually every nested optional can be represented with a single optional. Let’s visualize it:
In this hierarchy, only the last, the deepest optional is important for us isn’t it? Just by unwrapping the last one, we will know if it’s none
or its some
. And you can say: “But Swift is statically typed language and ?
and ??
are completely different types, the compiler cannot do that”. And it’s a valid argument, except Swift is already doing it :)
Optionals are treated specially by the compiler. In particular, a value of type T
is automatically wrapped into an Optional<T>
if necessary(E.G. You can pass Int
to a function that expects Int?
), it’s impossible for any other enum.
Also, you can always successfully cast optional of any depth to a single optional:
let a: Int???????????? = 10
let b: Int? = a as? Int
print(b) // 10
Another quite non-obvious example of flattening is optional chaining:
let a: Int?
let b = a?.signum().description
let c = a?.signum()
let d = c?.description
The result of chaining is optional, but we don’t need ?
after signum()
, but if we would do it with an intermediate variable we would need it. The compiler handles it for us.
Flatten nested optionals resulting from `try?` proposal added even more fuel to fire, it was implemented in Swift 5. Now we are getting single optional instead of nested after try?
.
func throwMeSmth() throws -> Int? { 0 }
let a = try? throwMeSmth() // Int? from Swift 5, before it was Int??
After that, it becomes even harder for me to justify why it could be done for try
but not for all nested Optionals. Despite all my confusion it actually makes sense. To understand why Swift is not coalescing it in all cases, let’s take a look at this example:
This code works and all values are printed in the console, but imagine this example with optional flattening. Condition in line 22 will fail because Int??
will be flattened to Int?
which is actually our first element nil
. Basically, the first element is turned into a stop condition, and while
loop breaks in the first iteration. Having double optional here is crucial because first Optional
represent the absence of value on Stack
level and second Optional
represent the absence of value inside ofT
itself.
So instead of looking at the Optional
as a simple none
/ some
, we should look at it through the context, e.g. for the stack example it will be smth like this:
Conclusion
Swift compiler doing a lot of optimization and syntax sugar for Optional
type which we as developers taking for granted. But even with all of that, flattening everything would cause a lot of problems in runtime, especially in generics.
I hope this article was interesting for you, If you enjoyed it, please consider giving it a clap 🤗