Conditionals control the flow of execution of your program based on conditions that you define. In Ruby we have conditional statements such as if
, else
, elsif
, unless
, case
, or the ternary operator.
Here I would like to focus on what I consider to be in most cases a questionable use of unless
and what I think could be a better approach. YMMV.
But first, I will start giving a bit of context about that conditional statement.
As far as I know, Perl was the first one that introduced unless
to provide a more readable alternative to using negated if
.
Although the keyword unless
is not available in most programming languages, Ruby already included it in its first version released in 1995. Other languages highly inspired by Ruby, such as Crystal or Elixir, include that control structure as well.
Its function is the opposite of the if
statement, meaning that will execute a block of code only if a condition is false. You could say it is syntactic sugar for a negated if
.
Remember that the only falsey values in Ruby, those that evaluate to false, are
false
andnil
.
I have read a lot about how unless
improves readability in cases where you are checking for negative conditions, making the code flow more naturally and even closer to plain English.
And I wonder, is that so?
Most of the people I have worked with come to Ruby from other languages, such as .NET, PHP, Python or Java. I have asked many of them what is their opinion about the use of unless
in Ruby and not a single one of them has shared with me a positive experience. Ever.
They usually have expressed confusion towards any code where that statement is used. Of course, you could think it is a small sample to draw many conclusions. However, if you search the web you will find other people as confused as my teammates.
I tend to agree, but I must say it was not always like that. In my first contact with Ruby, I kind of liked using unless
whenever I considered it was necessary without much hesitation.
However, nowadays I avoid its use as much as possible. In my opinion, it adds cognitive load in most cases. Every single time I see an unless
in the code I have to stop and translate it in my head to "if not". Even the easiest condition.
And I have been working with Ruby for almost 15 years now, no wonder it is difficult to grasp for people just learning the language.
In my side projects I completely avoid its use, but in a real job you need to reach an agreement with the rest of the team if you want the codebase to be as consistent as possible.
Although unless
is vastly used in the Ruby community, surprisingly I did not find much literature about it, with some exception. I am not taking into account the articles about the basic structures of the language, of course.
I would like to highlight that the author of the post linked above considers following code "little gems":
i += 1 unless i > 10
unless person.present?
puts "There's no such person"
end
Once again, I wonder if Ruby was designed with an emphasis on programming productivity and simplicity, why complicate things?
I would write the same code as follows:
i += 1 if i <= 10
if person.blank?
puts "There's no such person"
end
Much easier, right? To me it feels more natural and easier to understand. By far.
Whilst I disagree with his examples, I agree with his rules of thumb:
- Avoid using more than a single logical condition.
- Avoid negation as
unless
is already negative. - Never use an
else
clause with anunless
statement.
Next I will show some real-life examples with unless
along with the code I would write instead.
Probably the most common example is the use of the truthy value of a given variable:
return unless user
Nobody will ever convince me that previous line is more readable that the following one:
return if user.nil?
Other common example is with collections:
return unless posts.any?
# ...instead of...
return if posts.empty?
Another common example is with equality:
return unless currency == "EUR"
# ...instead of...
return if currency != "EUR"
Certainly there is a lot of personal preference here. What is readable for you may not be readable for me. And the other way around.
A good example could be the next one:
return unless user.confirmed?
# ...instead of...
return if !user.confirmed?
Personally, I prefer negating the condition in that case, but I think we should always stick to whatever the team decides.
I would even prefer the use of not
if what we are looking for is readability. Although I have never done that because the Ruby style guide has always discouraged it.
Yet another example would be the use of unless
along with else
:
unless user_signed_in?
render "login"
render "signup"
else
render "profile"
render "logout"
end
# ...instead of...
if user_signed_in?
render "profile"
render "logout"
else
render "login"
render "signup"
end
Fortunately, it is not so common to find such code.
The one rule I always follow, mentioned above, is to never use more than one logical condition with unless
.
def user_has_access_to_course?
return false unless user.present? && course.present?
user.purchased_course?(course)
end
# ...instead of...
def user_has_access_to_course?
return false if user.blank? || course.blank?
user.purchased_course?(course)
end
My brain is about to explode every time I run into that kind of conditions.
I also prefer to see any conditional upfront, so I try to avoid conditional modifiers as much as possible. Those conditionals are placed after another statement allowing to run the code only when the condition is met.
The obvious exception to that rule are guard clauses, mainly used to avoid a condition wrapping the whole body of a method:
def url
if course
course_url(course)
end
end
# ...or...
def url
course_url(course) if course
end
# ...but preferably...
def url
return if course.nil?
course_url(course)
end
Keep in mind that the code before the condition is simple in those examples, but I have found on countless ocassions a conditional at the end of a line with a length of 100 characters or more. That is not exactly what I would call readability.
Besides that when there are two possible paths in the code, I do not care if the code is written with a full if
/else
or a guard clause:
def url
if current_user.nil?
public_course_url(course)
else
private_course_url(course)
end
end
# ...alternatively...
def url
return public_course_url(course) if current_user.nil?
private_course_url(course)
end
But usually my teammates have prefered the guard clause, and I am ok with that option, as long as we are consistent.
As I said before, you have to reach an agreement with your team about how to write the code. In my opinion, a linter is the best possible tool to enforce those agreements.
Every single team I have been part of have always chosen RuboCop as linter. Out of the box it will enforce many of the guidelines outlined in the community Ruby Style Guide.
Next, I would like to highlight some cops that are related to conditionals:
- Style/IfUnlessModifier: Checks for
if
andunless
statements that would fit on one line if written as modifier and checks for conditional modifiers lines that exceed the maximum line length. I always disable it. - Style/NegatedIf: Checks for uses of
if
with a negated condition. Onlyif
s withoutelse
are considered. I always disable it. - Style/InvertibleUnlessCondition: Checks for usages of
unless
which can be replaced byif
with inverted condition. I always enable it. - Style/NegatedUnless: Checks for uses of
unless
with a negated condition. Onlyunless
withoutelse
are considered. I always enable it. - Style/UnlessElse: Looks for
unless
expressions withelse
clauses. I always enable it. - Style/IfUnlessModifierOfIfUnless: Checks for
if
andunless
statements used as modifiers of other conditional statements. I always enable it.
The related rules from the style guide are the following:
Hopefully I have given to you some arguments why unless
is not the best option to create readable code, but that is something very subjective, so the take away is to write conditionals in a consistent way, preferably following a style guide defined by the community or your own team.
Thank you for reading and see you in the next one!