To if or not to if, that is the question... unless?
The subtitle of this article is hopefully not only spotted as an unashamedly obvious butchering of Shakespeare’s famous line from Hamlet, but, also, and more importantly, recognised by Rubyists as a slightly humorous, albeit slightly more geeky, reference to how Ruby lets us
malform, abuse, truncate... make beautiful... the very humble and under-appreciated if statement.
Let’s say we want to return a string if something is true. In Ruby that if statement could look like this:
if something_is_true == true return 'result' end
For simple if statements like the one shown above, Rubocop, a Ruby code linter and general style guardian, would make a few suggestions. As this is the first example let’s take it nice and slow and remove that equals true Style/RedundantConditional as well as that return Style/RedundantReturn.
if something_is_true 'result' end
Great. So, the above code would only return the text string
result when something is
true and won’t return anything if it isn’t
true. If you’ve never delved into Ruby before, have a look at the following article on return values (it helps explain that the `result` is literally being returned, and, in Ruby, we don’t have to explicitly state a return for the code to be valid and to execute successfully).
Although we’ve got a legitimate if statement, our new friend Rubocop isn’t completely satisfied. As this is a single line if statement, we can still make a further improvement following the Style/IfUnlessModifier.
'result' if something_is_true
What… wait a minute… those three lines into one is great and all, but that looks weird!
In Ruby, this if modifier is one of the control expressions we can use to decide when code gets executed. When the if modifier is used together with an explicit return at the top of a method it is also known as a guard clause, but for our purposes here it is just important to realise that, although written very differently, the code will behave identically to the original statement. If you are seeing this sort of structure for the first time, it might be useful to take a moment to try and stay calm. Perhaps go and grab a cup of tea and try to remember that a guard clause is simply doing the following:
return 'result' if something_is_true == true
When I first started seeing guard clauses, the thought of actively putting ifs at the end of statements felt incredibly awkward. What I’ve come to realise, and, what I’m trying to convince you, is that these guard clauses are actually a great idea. However, with this great power comes great responsibility! I’ll discuss the irresponsible usage of guard clauses a bit later on, but for the time being, let’s get more acquainted with them in an even more unusual setting.
Already we have taken a standard if statement and effectively turned it back to front! But hang on, what about when we need to negate a condition? This sort of boolean funny business tends to happen quite a lot in software development, so, even though it feels slightly contrived, let’s change the original example with a bang (!) and say that we now will only return a string when somethingistrue is not true.
'result' if !something_is_true
Wait for it… brace yourself… yes, that did get confusing quickly, but let’s write that out in pseudocode to try and make it a bit clearer.
return ‘result’ if condition is not true
Which, after a few deep breaths, is all fine and functional, but yet again, our friend Rubocop wouldn’t just let us write any old code when there is an easier way Style/NegatedIf. Again in pseudo code, we could write the same instruction in a much more readable way e.g. always return the string unless something is true. That certainly sounds like it might be a bit easier and clearer.
'result' unless something_is_true
The above statement might look a bit strange, but fear not! Stick with me and let’s go through a quick refactoring to see how these simple Rubocop suggestions actually lend themselves to creating tighter, easier to read, and generally more elegant code.
def ugly_method if something_is_true 'Step 1...' else if !something_is_false 'Step 2...' else 'Step 3...' end end end
Now at first glance this doesn’t look too offensive, but keep in mind that this is really just illustrating what sort of monstrosities we can come up against. In the real world programmers are faced with tight deadlines and large legacy projects therefore the above example is really just demonstrating a stripped down version of more complicated steps.
I’m going to give this a justifiably harsh appraisal:
Ease of future adjustments ❌
If our friend Rubocop takes a quick look at this method, it suggests that using an elsif (Style/IfInsideElse) might improve things. If we take that advice we’d end up with something like this:
def better_but_still_ugly_method if something_is_true 'Step 1...' elsif !something_is_false 'Step 2...' else 'Step 3...' end end
Although perfectly acceptable this doesn’t really help us with the original caveat: trying to imagine the horror of multiple complicated actions within each of the steps. It’s also important to note that Rubocop doesn’t actually flag `better_but_still_ugly_method` as having any issues.
$ rubocop my_better_but_still_ugly_method_file_name 1 file inspected, no offenses detected
Despite Rubocop not flagging any issues, let’s take out those complicated steps and move them into their own private methods. Why do we do that? Well, really just to tidy it up and make it easier to deal with, but also to point out that it is very easy to fall foul of Rubocop’s suggested Metrics/MethodLength. To shift these steps into private methods, it is probably best to point out that we can only do that if we make a whole bunch of assumptions: that the original method is within a class and that these new private methods don’t need to be public (method visibility). We’re also going to assume that the new methods don’t need any arguments passed to them (see here for methods without args and the following for Rubocop’s thoughts on not using parentheses when you don’t have args Style/DefWithParentheses).
def almost_there_method if something_is_true step_1 elsif !something_is_false step_2 else step_3 end end private def step_1 'Step 1...' end def step_2 'Step 2...' end def step_3 'Step 2...' end
With our tidying of `almost_there_method`, the if statement conditions are starting to look a little bit empty and almost lonely. We can try and apply the guard clause logic that we were introduced to earlier to see what difference that actually makes.
A not so redundant return
Despite unceremoniously removing redundant returns in the earlier examples, we can now see just how useful `return` can be when combining it’s powers with guard clauses.
def beautiful_method return step_1 if something_is_true return step_2 unless something_is_false step_3 end
Here `beautiful_method` has retained the three steps from the original method all the while preserving the flow of each condition by exiting on the returns. We were able to utilise a classic guard clause for step_1, switch the negated if for an unless at step_2, and finish with the step_3 safe in the knowledge that it will execute when required. We have also snuck in some additional advice from Rubocop’s regarding the Layout/EmptyLineAfterGuardClause.
In summary, not only has this refactoring removed a lot of the unnecessary ends and elses, it’s also made it a whole lot more understandable!
I’m going to give this a justifiably great appraisal:
Ease of future adjustments ✅
The beauty here wasn’t just the joy we get from removing a horrible looking nested if statement, the real benefit comes when we realise that this refactoring has serendipitously led us down the path of creating code that is so much easier to test than the original. Beautiful_method’s purpose is now excruciatingly clear; it contains the logic to decide when certain steps get executed. Before it was a mixture of whatever each step included and the execution logic. Ugly_method was the sort of code that no-one wants to be wrangling with. Beautfiful_method is a piece of 🎂 to test. Another benefit is that future updates are also much easier. As this method only contains logic to control the execution of steps, if more steps are necessary we can easily add in additional clauses and, if the steps do change, beautiful_method doesn’t even need to be adjusted as each step has its own method!
Use your guard-ian powers for good not evil
There usually comes a time when great ideas go bad and guard clauses are no exception. Good intentions can quickly turn into monstrous acts of evil coding. In the examples below, the main point I’m trying to illustrate is that guard clauses work at the end of code blocks and that these code blocks can turn our previous harmony of simplicity into a contorted nightmare.
def beautiful_method_turns_to_the_dark_side return a_really_really_long_value_name ? step_1 : a_really_really_long_false_condition_name if something_is_true another_thing.tap do |a| a.generic_attribute = a_value a.run_another_method_on_another_thing return step_2 end unless something_is_false step_3 end
Looking at `beautiful_method_turns_to_the_dark_side`, the first line has acquired additional logic in the form of a ternary operator. This line sadly breaks our 120 character limit Metrics/LineLength, but more importantly step_1 is now embroiled in a reasonably complicated bit of boolean logic that obscures how and when the code actually gets run. Similarly, step_2 is now called as part of a tap block which has broken the Style/MultilineIfModifier rule.
Although all the changes are still perfectly valid Ruby syntax, our original beautiful_method has started morphing into something truly awful, and, worse still, is becoming increasingly difficult to understand and, more importantly, much harder to test.
Ruby syntax lets us handle flow and control actions within methods in really interesting and succinct ways. Making a few simple changes to structure can make big improvements to the readability and testability of our methods. Code linters like Rubocop try to prevent some of the more obvious crimes against eligibility, but with great flexibility comes great responsibility. Go forth and use your guard clauses confidently, frequently, but also cautiously.
I hope you found this article useful, and, if you’ve stayed until the end, I thought it might be nice to finish with a little example that not only uses all the logic covered in the above article, it summarises, or, perhaps more accurately, defines what daily life is like for us all here at Kyan.
def kyan return unless coffee return unless slack return unless jukebox return unless dogs return unless ruby || rails || react || design return unless product > awesome culture end
Previously from our Engineering Team:
How do you solve a problem like caching in Progressive Web Apps? by Dave Quilter
Rails 6: Seeing Action Text in... action by Stephen Giles
A Quick Comment on Git Stash by Karen Fielding
Avoiding N+1 queries in Rails GraphQL APIs by Andy West