Read This Post 'Unless' You're Not A Ruby Developer

Written on December 10, 2022

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 Zenbu, a web app that helps you manage your company's SaaS subscriptions. Your company is almost certainly wasting time and money on unused subscriptions and Zenbu can fix that. Check it out at zenbu.au