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
keyword.
In Ruby, 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.
Else blocks
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.
unless
hypnotises you into doubling-down on the negation
To expound on that last point, you often start with a whole condition negated (either via unless
of 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 if !
and people jumping back and forth between ruby and, say, javascript, now have one extra idiosyncracy to keep in mind.
Too many ways to do the same thing
If we’re already able to do if !foo
, why do we need unless foo
?
- 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 unless
over 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!
Shameless plug: I recently quit my job to co-found Subble, a web app that helps you manage your company's SaaS subscriptions. Your company is almost certainly wasting time and money on shadow IT and unused licences and Subble can fix that. Check it out at subble.com