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
andandorkeywords 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:
! |
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
end
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
unlesswithelse.
Avdi Grimm in a twitter conversation:
in 10 years of Ruby I’ve never seen [an instance where
unless ... elseis fine].
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?"
else
puts "and now we're really confused"
end
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:
- The condition reads better in English under
unlessthan withif; - I expect the
unlesscode block to run more frequently than theelseblock.
# some perfectly valid code, in my book
unless response.redirect?
# process response.body (the more common case)
else
# follow response['location']
end
However, Konstantin Haase raises a valid argument:
if
unless ... elsewould make sense, thenelsunlesswould 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.