Ruby: Safe Navigation Operator and Methods

Even though Ruby 2.3 was released some time ago, it wasn’t until recently that I came to discover some of the new features that it brought, such as the safe navigation operator & and the Array#dig and Hash#dig methods. In this post, I’m going to explain how this new operator and the methods allow you to write more concise code.

Safe Navigation Operator &

As its name reflects, the safe navigation operator allows us to safely call a method on a object that can be nil, in the same way as try! does in ActiveSupport. In that regard, if the object is not nil, it sends the method to the object, returning nil otherwise.

For example, if we want to check whether the last element of an array is an odd number:

=> [1, 2, 3].last.odd?
# => true

If the array is empty, we’ll send the method :odd? to a nil object:

# when the array is empty, last is nil
=> [].last.odd?
# NoMethodError: undefined method `odd?' for nil:NilClass

This can be fixed by checking if the last element exists before:

=> [].last && [].last.odd?
# => nil

# or DRYing a bit
=> (last_element = [].last) && last_element.odd?
# => nil

With the safe navigation operator, the previous code would looks like:

# with the safe navigation operator
=> [].last&.odd?
# => nil

This comes handy when checking chained objects calls, since the safe operator will return nil for the first nil object that it encounters, removing the necessity of checking for nil objects:

if user && user.address && user.address.postcode
# ...
end

# or with ActiveRecord :try
if user.try(:address).try(:postcode)
# ...
end

both could be replaced with the safe navigation operator:

# with &. safe navigation operator
if user&.address&.postcode
# ...
end

This makes the code more concise, and it removes the dependency on ActiveSupport for the try! method if you are not running a Rails application.

Array and Hash :dig

The Array and Hash classes were also extended in Ruby 2.3 with the :dig method, which also relates to safe navigation for multi-dimensional arrays and nested hashes.

In both cases, dig accepts a variable number of arguments, where each of the arguments represent a level or dimension to be accessed, the same way as when chaining [] access calls to an array or hash, but with the difference that it will fallback to nil instead of throwing a NoMethodError if any of the accessed level is missing.

For example, if we have a multi-dimensional array:

=> my_array = [[1,2]]

=> my_array[0][0]
# => 1

=> my_array[3][0]
# => NoMethodError: undefined method `[]' for nil:NilClass

Using dig

=> my_array = [[1,2]]

=> my_array.dig(0, 0)
# => 1

=> my_array.dig(3, 0)
# => nil

And in the case of hashes:

=> my_hash = {
     foo: {
       bar: 42
     }
   }

=> my_hash[:foo][:bar]
# => 23

=> my_hash[:foobar][:bar]
# => NoMethodError: undefined method `[]' for nil:NilClass

Using :dig will fallback to nil when one of the chained keys is missing.

=> my_hash.dig(:foo, :bar)
# => 23

=> my_hash.dig(:foobar, :bar)
# => nil

Conclusions

Both the safe navigation operator and the Array#dig and Hash#dig methods help in removing the necessity of checking for nil objects, making the code more elegant and concise.

It personally took me a bit in the beginning to get used to see the safe navigation operator in the code (even my Ruby linter complaint about it), but it has now become a standard element in my Ruby toolbox.

Portrait picture

Andy Haxby • Founder

Andy is the founder of Competa and FTSF. He is always looking to find ways to improve sustainability with software.