Molly Struve (she/her)
Posted on May 29, 2019
We have all been there, we issue a Pull Request(PR) that we feel is super tight and awesome. It gets stamped "Approved", but there is a catch. Our code isn't actually as tight as we thought. We open up our PR to a bunch of very nice suggestions on how we could tighten up our code even more.
This is VERY common when you are starting out and it is actually how I learned many of these tricks. Thanks to a lot of very good senior devs reading and commenting on my code from the very beginning, I have learned how to write pretty succinct code. But I will let you in on a secret, it never ends. I have been doing this for 7 years and I STILL get suggestions for how I can write better code.
Below are some common ways that you can take basic Ruby logic and tighten it up with some of the syntactic sugar. Syntactic sugar is syntax within a programming language, like Ruby, that is designed to make things easier to read or to express.
- Booleans/Conditionals
- Variable Assignment
- Guard Clauses
- Calling Methods
- Safety Navigator
- Hashes and Arrays
Booleans/Conditionals
Ternary Operator
Let's start with some basic boolean logic in order to set a variable. If y
is even we want to set x
equal to 1, if y
is odd we want to set x
equal to 2. Here is how we are taught to do it when starting Ruby.
if y.even?
x = 1
else
x = 2
end
The above gives us two assignment statements. This means if we have to change our variable x, we have to make the change in two places, which is not ideal. One way we could simplify the code is by doing this:
x = if y.even?
1
else
2
end
Now, we only are making our x
assignment once, which is a little more simple. But even with the above, we are taking up 5 lines to do it. Let's see how we can take it down to one line.
x = y.even? ? 1 : 2
?
is called a Ternary operator. The Ternary operator logic uses "(condition) ? (true return value) : (false return value)" statements to shorten your if/else structure. It first evaluates an expression as a true or false value and then executes one of the two given statements depending upon the result of the evaluation. Here is the syntax:
test-expression ? if-true-expression : if-false-expression
This can be extremely useful when it comes to tightening up logic statements.
Inline Booleans/Conditionals
In addition to the slick ternary operator, let's look at another way we can condense some boolean logic into a single line. Here is a basic method with some boolean logic.
def say_number_type(number)
if number.even?
puts "I am even!"
end
end
To make this more compact we can write that boolean like this:
def say_number_type(number)
puts "I am even!" if number.even?
end
The same can be done with unless
def say_number_type(number)
puts "I am even!" unless number.odd?
end
NOTE: Using inline booleans or conditionals is sometimes referred to as using "modifier syntax".
Variable Assignment
Let's say we have a scenario where we want to assign a variable to another based on if the other variable is present. For example, we have variable a
and b
below. If a
is present(not nil) we want to set x
equal to a
. If a
is nil, we want to set x
equal to b
. We could do something like this.
if a.nil?
x = b
else
x = a
end
Or, given what we learned above we could do
x = a.nil? ? b : a
However, when dealing with possible nil values we can make this even more succinct by using the pipe operator like this:
x = a || b
If a
is not nil, that assignment statement will assign the value of a
to x
. If a
is nil, it will assign the value of b
to x. Checkout the console examples below.
irb(main)> a = 1
=> 1
irb(main)> b = 2
=> 2
irb(main)> x = a || b
=> 1
irb(main)> a = nil
=> nil
irb(main)> x = a || b
=> 2
We can use the pipe operator between two variables and we can use it with an equals. Let's say we want to set x
equal to a
but only if x
is nil. If x
is not nil we want to preserve the value of x
. In order to accomplish this we would do:
x ||= a
Here is a console example showing exactly how this works:
irb(main)> x = 1
=> 1
irb(main)> a = 2
=> 2
irb(main)> x ||= a
=> 1
irb(main)> x = nil
=> nil
irb(main)> x ||= a
=> 2
This is telling ruby to assign the value of a
to x
only if x
is nil. This can be helpful when you are expecting a default but can't rely on it being there. Let's say you have the below method
def add_one(number)
number + 1
end
If number
is nil then we will get a NoMethodError
when we try to add one to it. We can avoid raising an error by doing something like this
def add_one(number)
number ||= 0
number + 1
end
number ||= 0
will ensure that if number
is not defined our method will not raise an error because it will be set to 0. If you want to take this one step further, you can put the assignment operator IN with the argument! 🤯
def add_one(number = 0)
number + 1
end
When you are calling this method, if you do not pass it a number, ruby will set number equal to 0.
irb(main)> add_one
=> 1
irb(main)> add_one(1)
=> 2
Guard Clause
A Guard Clause is a premature exit from a method if certain conditions are met. Guard clauses can help you condense your method logic and write more optimized code. Let's look at an example of when we might use one. Say you have the below method tester
def tester(arg)
if arg == 1
# logic
else
'error'
end
end
In this case, if our argument, arg
, is not equal to 1 then we will return a string that says 'error'. We can simplify this tester
method by using a guard clause which is shown below.
def tester(arg)
return 'error' unless arg == 1 # Guard Clause
# logic
end
When we start our method we immediately check if the value of arg
is equal to 1. If it is, we return immediately. No need to fumble around with everything else in the method since we know what we want to do. This can be especially helpful when it comes to optimizing code. For example, keep an eye out for situations like this:
def tester(arg)
user = User.find(123)
event = Event.find(123)
if arg == 1
# logic
else
'error'
end
end
Here, we are gathering data and executing logic before we even get to our if/else boolean. In the end, if arg
is not equal to 1, then we just wasted a lot of time and resources gathering data we didn't need. A guard clause can ensure that none of these resources are wasted by checking your condition in the first line of your method.
One more tidbit I want to add is if you want to use a guard clause to return nil based on a condition, you can do this:
def tester(arg)
return unless arg == 1
# logic
end
In the above case, return
alone is the same as if you wrote return nil
.
def tester(arg)
# nil is unnecessary
return nil unless arg == 1
# logic
end
In Ruby, however, you don't need to explicitly define nil in this case. The first example, return unless arg == 1
is how you will want to write it and how you will likely see it written by others.
Calling Methods
Let's say you are working with a set of objects that respond to a method and you want to collect the result of that method using a method like map
. Normally, you would do something like this:
a = [1, 2, 3, 4]
string_list = a.map{|obj| obj.to_s}
# string_list = ["1", "2", "3", "4"]
We can simplify this even more using a little syntactic sugar and still get the same result.
string_list = a.map(&:to_s)
# string_list = ["1", "2", "3", "4"]
Any method that our set of objects, in this case integers, respond to can be called on each object using &:method
syntax. If you want to understand what Ruby is doing under the hood, I recommended giving this blog post a read.
Safety Navigator
How many of you have written code like this before? ✋ I definitely have!
def friends_name(user)
return unless user.present?
friend = user.friend
if friend.present?
friend.name
end
end
There is a lot of presence checking going on here just to ensure we are never calling a method on a nil object and getting the dreaded # NoMethodError
. Rather than doing all of this checking, we can use Ruby's Safety Navigator! The Safety Navigator will simply return nil if the method is called on a nil object. This means we can simplify our method to this:
def friends_name(user)
user&.friend&.name
end
If any object in that chain is missing we will simply get nil back. Here is another simple console example that will hopefully help further clarify how the safety navigator works.
irb> [].last.to_s
=> ""
irb> [].last.even?
NoMethodError: undefined method `even?' for nil:NilClass
from (irb):3
from /usr/bin/irb:11:in `<main>'
irb> [].last&.even?
=> nil
Hashes and Arrays
Brackets vs do/end
In my Hashes and Arrays tutorials I chose to use a lot of do/end block notation to help make it clear what logic was taking place. However, bracket notation is a great way to write more compact code. As we saw in the tutorials, it is a great way to chain together a bunch of logic.
result = [1, 2, 3, 4].map{|n| n + 2}.select{|n| n.even?}.reject{|n| n == 6}
# result = [4]
Now you might be thinking given all these options, how do I know what to use?! Usually, a good rule of thumb is:
- If one line of code is being executed in the block then use brackets.
- If your logic is more than one line, use do/end notation.
Single line bracket example:
a = [1, 2, 3]
a.map{|number| number + 10}
Multi line do/end example:
a = [1, 2, 3]
a.map do |number|
if number.even?
puts "I am even"
number/2
else
puts "I am odd"
number
end
end
Keep in mind that some places or people might have a different style. Always be aware when you are starting at a new place what others are doing. You will likely want to follow their lead when you are starting out. As you gain more experience you will develop your own style and habits.
Chaining
Really quick I want to loop back to the chaining example I used above:
result = [1, 2, 3, 4].map{|n| n + 2}.select{|n| n.even?}.reject{|n| n == 6}
# result = [4]
This code is pretty straight forward, which is why chaining each step is a great choice to make it more compact. However, there will be times you actually might want to split them up for readability if the logic in each step is complicated. Here is an example of how you would split it up for readability.
plus_two_array = [1, 2, 3, 4].map{|n| n + 2}
even_numbers_array = plus_two_array.select{|n| n.even?}
remove_the_sixes_array = even_numbers_array.reject{|n| n == 6}
Each step and its result are clearly defined which can make more complicated logic easier to follow and understand. The reason I want to point this out is because there will be times when writing the most compact code is not best for a situation. All these strategies are great, but be aware there will be times when you might want to sacrifice compact code for readability.
But Wait, There's More!
Obviously, there are tons more ways you can write more concise and compact code. This is only a very small list, but I think it is a great place to start. If you have found other tricks that have helped you improve your code feel free to drop them in the comments. As always, if you have questions or anything needs clarification please don't hesitate to ask!
If you want a quick reference for these methods I made a handy cheatsheet with just the code from each example.
Posted on May 29, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.