Designing to avoid Optionals in Swift
Back in the Objective-C days, any object variable could be nil
. If you sent a message to
a nil
pointer, the method basically turned into a no-op (returning nil
again, so nested
Objective-C calls like [[MYClass alloc] init]
would just collapse into a no-op if the
first call failed).
This seemed a convenient way to do error handling: Just return nil
on error and whoever
calls you doesn’t have to handle errors at every step of the way. However, it was also
so very implicit, that often programs encountered errors and just … stopped reacting.
Some variable had ended up being set to nil
, and the app just kept calling into the void,
where nothing happened.
Enter Swift
Swift learned from this, and made nullability an explicit attribute of a variable’s type.
If a variable could become nil
, the API contract now said so. If you tried to use a
nullable value without checking that it wasn’t nil
beforehand, that was a compiler error.
But since Swift was used on top of the Cocoa frameworks, which were written in Objective-C,
many properties were marked as nullable that actually could never become nil
. But since
the Objective-C code didn’t mark up nullability, how was Swift to know? So nullability was
retrofitted into Objective-C, and the system headers marked up, just enough so Swift could
know which properties needed to be optional and which didn’t.
Still, I see a lot of Swift code that basically contains “extra line noise” in the form
of nil
checks that “should never happen”. Code that is basically replicating Objective-C’s
nil
-collapsing behaviour in a more verbose way. There is no recovery from these cases.
Often they are guard
statements that simply abort execution of the current method.
This is good: It is explicit. As soon as you read the line, you see that the function will abort in this degenerate error case.
This is bad: A lot of code that is never run, or is only run in a debug build when you make a mistake editing some code, clutters up the lines of a method, making it harder to follow the actual intent of the method.
Furthermore, methods that return a value often need to “lie” about their result upon such a failure. If they don’t, they in turn become failable (and their return types optional):
If a method they call can fail in the degenerate case, the method around it can fail in that same case. At least in Swift this is now explicit in the code, but it also means that we’ve basically arrived back at procedural C’s error handling, which cluttered up our algorithms, when the reaction to each failure usually ended up being the equivalent of “I’m sorry, Dave. I’m afraid I can’t do that.”
This is why throwable errors (like C++ exceptions and Swift errors) were introduced: To
move the (usually identical) error handling out-of-band and reduce the amount of code
dealing with error checking in favor of the code actually handling the errors.
Our guard
or if
block is hidden behind that tiny try
statement.
I don’t know why exactly, but throwable errors seem to have a bad reputation among Swift
programmers. Admittedly, if you look at C++ exceptions, it is very easy to overlook who
may throw. But Swift has the try
label on failable lines, so can that really be the reason?
Is it a holdover from Objective-C and Swift 1.0, where exceptions were only used for programming errors and system code was simply not prepared to handle them for mostly historical reasons? Is it a dislike for the added exception mark-up that is less subtle than making a variable’s type optional and adds deeper nesting?
Is it maybe an awareness of the fact that, once everything can fail, it becomes hard again to tell what actually will fail, and when, and we’re back where C++ is, where nobody will really deal with failures that “shouldn’t happen”? But isn’t that where we already are with all those optional checks?
Fail Early
I think current Swift code still suffers from being architected like it was Objective-C when
it comes to optionals, but while the cognitive overhead of optionals (nil
messaging) was
comparatively low beyond reading a line and doing a quick “what happens if this goes nil
“-check,
optionals in Swift cost us time blanking their associated checks out so we can
“see the algorithm between them.”
This is odd, given Swift is at its core a language that prefers a high density of operations per statement, and makes a lot implicit (like variable types, through inference).
Whether you like the number of operations Swift crams into a single statement, that is
definitely what the language was designed for, and I think we should try to strive for
this level of focus in our algorithms as well. And that to me includes designing them to
avoid optionals (and their associated error checks) where it furthers clarity.
Make it invalid to even start running a method or algorithm before we’ve checked that
our parameters aren’t nil
. This up-fronts the error checks and chances of failure,
instead of littering them all through our code.
Similarly, this should be possible with callbacks. Design our methods so they have separate
result and completion callbacks. If there are no results, we then don’t have to give a nil
result
in a completion block that they have to check for, but rather just don’t call the result callback
at all. Alternately, if we’re dealing with collections, we can give empty arrays
instead of a nil
array. Then anyone iterating over the list just doesn’t do anything.
In its own way, these two techniques are like Objective-C’s nil
-collapsing again. Of course,
that means it is similarly implicit. What if the loop contains a call that must be done,
even for 0 items? Then we would again have to add a conditional for the “0 items” case
that covers it. Might as well let the compiler remind us of that, then, and make it an optional.
But where we can front-load these checks, our algorithms can become a lot clearer, simpler, at the back end of the equation. Fewer optional checks. More raw control flow dealing with the core of our functionality.
Optionals as a code smell
Given most projects have existing code that isn’t front-loaded yet, and we’re writing
against operating system frameworks that were designed to take advantage of Objective-C’s
nil
-collapsing, front-loading nil
-checks is not something we can just do in a week of
refactoring.
But I think all it takes to improve our Swift code is to raise awareness of this issue, to make Swift programmers pause each time before they declare an optional and think:
“An optional! Ewww! Can I somehow make this non-optional or front-load it?”
If optionals are treated like any other code smell, I think our Swift code will put our actual application logic back in the spotlight, and thus help us detect the complex bugs instead of hiding them among lines of “this should never happen” guard statements.