Level Up Your Ruby Skillz: Writing Compact Code

molly

Molly Struve (she/her)

Posted on May 29, 2019

Level Up Your Ruby Skillz: Writing Compact Code

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

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

? 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The same can be done with unless

def say_number_type(number)
  puts "I am even!" unless number.odd?
end
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Or, given what we learned above we could do

x = a.nil? ? b : a
Enter fullscreen mode Exit fullscreen mode

However, when dealing with possible nil values we can make this even more succinct by using the pipe operator like this:

x = a || b
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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} 
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
molly
Molly Struve (she/her)

Posted on May 29, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related