A lot of what I do for my clients is code review. Code review is one of the better ways we know of to catch bugs early. I’ve been doing more code review in my day-to-day, and I’ve started to notice patterns arising out of the mire — little things that I see and can flag for deeper attention. One of these little smells is the case of the missing else branch.
Let’s say we’re trying to enable a button when a UITextField has more than 8 characters:
@objc func textChanged(_ sender: UITextField) {
if (sender.text ?? "").count >= 8 {
submitButton.isEnabled = true
}
}
There’s a bug here. UIKit warriors have probably seen this particular bug many times and solved it an equal number of times. But because our programming tools are stuck in the Dark Ages, we can’t see the bug, so let’s spin up a little virtual machine in our brains. We’ll need to keep track of the text of the sender
, the user’s keyboard entry, and the enabled state of the submitButton
.
What happens when if the user types their 8 characters and then deletes one? The button becomes enabled, but stays enabled when they delete a character.
The bug is hard to spot, but, if the title of the blog post doesn’t give it away, easy to fix.
@objc func textChanged(_ sender: UITextField) {
if (sender.text ?? "").count >= 8 {
submitButton.isEnabled = true
} else {
submitButton.isEnabled = false
}
}
Now, when the user types in their 8 characters and deletes one, the submit button is correctly disabled. Leaving out the else
branch meant that a path of code wasn’t fully handled. Swift lets us elide the else
branch, which it conceals its true nature: the code path was empty.
Let’s look at another example. This time, we do have an else
branch:
if let location = venue.location {
cell.addressLabel?.text = location.address
} else {
cell.addressLabel?.isHidden = true
}
At first glance, the code seems sensible. If there’s no address, hide the addressLabel
. That probably causes some side effect in layout, maybe moving some neighboring views around.
But similar to the last example, there’s a bug. Let’s spin up another virtual machine to model all this state. This particular code has an issue on reuse. What happens if the cell is used for a venue without an address, then used for a venue with an address?
The addressLabel
is hidden and never unhidden.
Like the last snippet, we could solve this by adding another line of code, this time to our if
branch rather than our else
branch. However, I think the code is served better by assigning the isHidden
property to a computed boolean, instead of manually branching on that boolean with an if
/else
. Here’s what that looks like:
if let location = venue.location {
cell.addressLabel?.text = location.address
}
cell.addressLabel?.isHidden = venue.location == nil
We can even take this a step further and remove the branching entirely, since text
accepts an optional String:
cell.addressLabel?.text = venue.location?.address
cell.addressLabel?.isHidden = venue.location == nil
Much simpler, with zero branching.
It should be said that the first snippet can be written in this terse style as well. This is how I prefer to write it:
@objc func textChanged(_ sender: UITextField) {
submitButton.isEnabled = (sender.text ?? "").count >= 8
}
What’s interesting about this class of bug is that some languages simply don’t let you write code like this. For example, Haskell has an if..then..else
construct, and the else
branch is required. This Haskell won’t compile without the last line:
if a
then b
else c
Haskell’s if..then..else
is an expression, so it has a result (either b
or c
in this case) and that result has a type. Without an else
branch, what would that result evaluate to? It’s an impossible question to answer, so Haskell makes it invalid at a code level as well. (A functional way to think about this: side effects aren’t allowed in Haskell. In the same way a Void-returning function must perform a side effect (or do nothing), an if
with no else
must also perform a side effect.)
You can approximate this behavior in Swift by returning something — you have to include an else
branch. For example, this code won’t compile. How could it?
func addressLength() -> Int {
if let address = self.address {
return address.count
}
} // Missing return in a function expected to return 'Int'
If you think about the myriad ways Swift gives you to handle branching — stuff like guard
, assigning a boolean, ternaries, and of course, our friendly neighborhood if
statement — the if
statement is the only that lets the user leave out one of the branches. Even switch
requires you to either exhaustively handle all cases or include a default
case. From that perspective, leaving the else out is almost syntactic sugar. In some ways, it’s comparable to using a exclamation point to force unwrap. You can do it, but you’re leaving code paths unwritten.
Writing an if
with no else
isn’t wrong per se, it’s just a smell. When you see it, give it a second pass and really make sure that if it were there, the else
branch would actually have no code in it.