Read This Post 'Unless' You're Not A Ruby Developer
Disclaimer: prepare for hair-splitting and nitpicking
If you like this post, check out the sequel
I don’t like Ruby’s
unless is just syntactic sugar for
if !, so:
unless foo && bar # do something end
is the same as
if !(foo && bar) # do something end
Given that we have an
if statement which executes on true values, it seems sensible to have a corresponding
unless statement that executes on false values. But I find that it causes more trouble than it’s worth.
Here are my reasons:
Concealed double negatives
The human brain, impressive as it is, grinds to a halt when parsing with double negatives. It’s amazing that we’ve achieved so much as a species despite this embarrassing shortcoming. So we should take any chance we have to reduce the double negatives in our code. Unfortunately,
unless is a magnet for double-negatives.
Geeksforgeeks (admittedly, not the best source) illustrates the use of
unless with the following example:
unless condition # do A else # do B end
I don’t know why anybody would want to use
unless in conjunction with an
else block. If I come across the following (equivalent) code:
if !condition # do A else # do B end
It’s obvious to me that there’s a double negative: one from the bang (exclamation mark) and one from the
else. You can easily resolve that by negating the condition and swapping the legs:
if condition # do B else # do A end
Which is easier to read? If somebody says to you ‘if it’s raining, bring an umbrella, otherwise leave it at home’ that’s a dead simple instruction. If somebody tells you ‘unless it’s raining, leave your umbrella at home, otherwise bring it’ that’s a brain teaser.
Adding extra conditions
Say you want to return ‘invalid’ if a token isn’t valid. You could write either of the following:
return 'invalid' if !valid_token return 'invalid' unless valid_token
You may find the second option, with
unless, more readable. Fair enough. But let’s say that later on you want to also check that the token hasn’t expired. With the
if approach it’s easy to tack on a condition:
return 'invalid' if !valid_token || expired
But with the
unless approach, it’s not so simple:
return 'invalid' unless valid_token && !expired
The reason you might find that snippet confusing is because of the concealed double negative. The statement really says:
return 'invalid' if !(valid_token && !expired)
Using De Morgan’s Theorem (i.e.
!(a && b) == !a || !b), we can rewrite the statement as:
return 'invalid' if !valid_token || expired
Which we already had above when we didn’t use
unless to start with!
What’s going on here? When you use
if, you’re committing to expressing something in positive terms. That conveniently makes it easy to add more conditions as needed. On the other hand, with
unless you’re committing to expressing something in negative terms (by negating the whole expression). This means:
- adding a new condition is not simple
- as soon as you add a negated condition e.g.
&& !expired, you’ve introduced a double negative
Note that the exact same arguments apply when instead of using
unless you’re using
if !(...), but the latter approach has a couple of advantages:
- when you have a double negative, it’s dead-easy to spot by just looking at the bangs.
unlesshypnotises you into doubling-down on the negation
To expound on that last point, you often start with a whole condition negated (either via
if !(...)) and later on the condition grows and ends up being better expressed in positive terms. If you’ve gone with
if !(...), then expanding out a bang (using De Morgan’s Theorem) is a straight-forward process, but even though the refactor is exactly the same with
unless, from my personal experience reviewing Ruby code, there’s this bizarre quirk of human psychology where developers retain the
unless against all odds. Not unlike Isildur from Lord of the Rings, developers who have the chance to cast
unless into the fire instead stick with it despite the fact that refactoring to an
if would vastly simplify the now unwieldy expression.
Perhaps the appropriate response to that is for humans to just be better, but anybody who’s seen Lord of the Rings knows that humans are ever prone to temptation.
Incompatibility with other languages
As a general rule I think that if a language has some feature for which there is already a commonly understood syntax across other languages, it should just use that syntax. If you’re introducing a complete paradigm shift, then that’s fine, but
unless is not that: it’s just a different way to write
Too many ways to do the same thing
If we’re already able to do
if !foo, why do we need
- it’s more syntax to learn
- it’s an extra choice you need to make when writing code
- it’s a source of stylistic disputes which make people write ranty blog posts instead of doing something productive.
English meaning is slightly different
In the examples above, I claim that going without
unless makes it easier to read if statements as plain English. But when there are bangs in there, you need to do some translation, so surely in the basic case of
unless foo, that’s more readable than
if !foo? Not necessarily. In English,
unless doesn’t just mean
if not: it suggests that an action is exceptional, unlikely, or unexpected (see this rubocop style guide thread for an extended discussion on this point). So, taking the example from that thread, comparing the following two options:
if !user.admin? raise "Unauthorized" end unless user.admin? raise "Unauthorized" end
I find the second option less readable because it suggests that raising the error would be the normal thing to do, when in fact it’s the exceptional thing to do.
Okay fine, but doesn’t that mean that
unless is superior if the action is the normal thing to do? For example:
if !user.suspended? send_email end unless user.suspended? send_email end
I’m happy to concede that point. But if that’s the only benefit of
unless, that it’s more readable in some circumstances, I don’t see how that compensates for all the downsides I’ve talked about above.
Why does this matter?
Rubocop’s Ruby Style Guide says to prefer
if for negated expressions:
# bad do_something if !some_condition # bad do_something if not some_condition # good do_something unless some_condition
To its credit, the guide grants an exception for when
else blocks are involved, but I still think the recommendation is mistaken.
unless provides some small benefit when you’ve got a single condition and you’re talking about an exceptional case e.g.
open unless door_is_locked, but I don’t think that benefit is worth the awkwardness that arises in other contexts.
This post is not a call to arms to try and get any style guide to change, because reading through some of the comments on the topic, there are people who find
unless more readable compared to
if ! in the vast majority of cases. But my experience has been the exact opposite, and with all of my posts that nitpick at some language feature (looking at you, Go’ing Insane series) I really just want to see if other people relate to my experience. So if you’re reading this and you’re a Ruby dev, whether you love or hate
unless, let me know your thoughts!