The controversy of "and or unless else"

There are two features of Ruby that are often frowned upon by experienced Rubyists: the and and or keywords, and the unless ... else construct.

I want to argue that both are OK and, when used correctly, need not be considered a code smell.

Ruby’s and and or are not voodoo

Every programmer learns at some point that and and or keywords are not synonyms for && and || operators. Unfortunately, they usually learn it the hard way, usually by trying to use them before assignment:

# danger: will not work as expected
show_help = args.empty? or args[0] == '--help'

This code seemingly does what it was supposed to, but has a subtle bug: it does not respect the --help flag.

Now the newbie programmer got burned, and asks “If and and or aren’t synonyms for operators, what are they?” Seasoned programmers then, as an answer, mumble something about “precedence”, and they add “You must never use them again.” From GitHub’s Ruby style guide:

The and and or keywords are banned. It’s just not worth it.

A hard rule like this is not very satisfying advice. (And yes, GitHub, when you open-sourced your style guides, you turned them from internal documents to advice for the community.)

The real answer is those keywords perform the same function as the operators, but they have different precedence–therefore they have different use-cases.

In fact, let’s demystify them right away:

A selection of Ruby operators, high to low precedence
! not
* / % multiply, divide, & modulo
+ - plus & minus
<= < > >= comparison
<=> == != equality
&& logical 'and'
|| logical 'or'
? : ternary
= assignment
not logical negation
or and logical composition

They’re right there in the bottom. If you are required to remember that multiplication happens before addition in a + b * f, why not be aware that assignment has higher precedence than and?

Avdi Grimm argues that those keywords, originating from Perl, were intended to be control flow operators. I fully subscribe to this way of thinking, and often use this and similar patterns in flow constructs:

if name = params[:full_name] and !name.empty?
  # do something with name

Know your language well, and expect of others to know it, too. If beginner programmers in your group stumble on this, help them out like you would help with any other Ruby concept that isn’t obvious (e.g. in class << obj syntax, the << operator is neither shift nor append). It’s not such a big hurdle.

From Programming Perl:

The moral of the story is that you must still learn precedence (or use parentheses) no matter which variety of logical operator you use.

The case of unless ... else

GitHub’s style guide:

Never use unless with else.

Avdi Grimm in a twitter conversation:

in 10 years of Ruby I’ve never seen [an instance where unless ... else is fine].

37signals blog:

Never, ever, ever use an else clause with an unless statement.

[…] as with anything that gives you a little power, it can be abused.

Jamis from 37signals offers an intentionally convoluted example to prove their point:

unless !person.present? && !company.present?
  puts "do you even know what you're doing?"
  puts "and now we're really confused"

I’m sure Jamis is able to deliberately design horrible code that can make any feature of Ruby look like an “abuse of power”.

But as with and and or, unless ... else isn’t some highly sophisticated, magical voodoo construct that requires extreme concentration to wrap your head around, and is best avoided to keep code clarity. It’s just if ... else reversed.

I’m using unless ... else when it fits, and I’ve got two simple criteria to decide if that’s the case:

  1. The condition reads better in English under unless than with if;
  2. I expect the unless code block to run more frequently than the else block.
# some perfectly valid code, in my book
unless response.redirect?
  # process response.body (the more common case)
  # follow response['location']

However, Konstantin Haase raises a valid argument:

if unless ... else would make sense, then elsunless would make sense, too.

It’s true that elsif is only valid in if constructs, and there is no counterpart for unless. However, I don’t miss it, as I can’t imagine how it would ever read well in English.

Update: Avdi Grimm responds explaining his thought process behind improving the specific example above by avoiding control flow blocks altogether. I agree that careful refactoring can often reduce the amount of control flow in favor of describing the logic in the language of the domain.