Safe navigation in Ruby
Navigation through embedded objects can suprise us with an error if one of the fields, which are objects as well, is null. We are in the same situation if we have a hash object and want to get a value by key if key does not exist. The burning situation crops up if we chain up methods next to each other.
In this post I write some approaches, how to keep our code still clean, readable and safe if we liked to chain up calls next one another on an object or on a key-value pair container.
Demonstration of the core problem comes below:
Address = Struct.new(:city, :street, :number)
Person = Struct.new(:name, :age, :address)
people = []
people[0] = Person.new('jancsi', 12, Address.new('City', 'somewhere over the rainbow', '42'))
people[1] = Person.new('jancsi', 12)
people.each do |person|
p person.address.street
end
# => 'somewhere over the rainbow'
# => `<main>': undefined method `street' for nil:NilClass (NoMethodError)
And in case of hash, the same problem looks like that:
animals = Hash.new
animals['mammals'] = {'even-toed' => 'giraffe'}
animals['reptiles'] = {'snakes' => { 'cobra' => ['king cobra', 'tree cobra']} }
['mammals', 'reptiles', 'birds'].each do |key|
p animals[key]['snakes']
end
# => nil
# => {"cobra"=>["king cobra", "tree cobra"]}
# => Traceback (most recent call last):
# ...
# => navigation.rb:29:in `block in <main>': undefined method `[]' for nil:NilClass (NoMethodError)
Solution in Ruby
Ruby introduced the safe navigation operator &
in version 2.3, with which we
can handle null objects.
people.each do |person|
p person&.address&.street
end
# => "somewhere over the rainbow"
# => nil
What if we liked to get a value based on its key and chaining the indices next one another.
If one of the key is missing, indexing would be applied against a null object.
Furtonately, the ||
operator comes handy.
p (animals['mammals'] || {})['snakes'] # => nil
p (animals['reptiles'] || {})['snakes'] # => {"cobra"=>["king cobra", "tree cobra"]}
p (animals['birds'] || {})['snakes'] # => nil
If the value belongs to the set of falsy values like false
we get the same result,
though false
can be a valid index.
Let us see what we can do to enhance fetching values by keys from a hash.
Hash
types brings dig
method:
p animals.dig('mammals', 'snakes') # => nil
p animals.dig('reptiles', 'snakes') # => {"cobra"=>["king cobra", "tree cobra"]}
p animals.dig('birds', 'snakes') # => nil
Gems
Older versions of Ruby (below 2.3) do not support safe navigation, so gems
had been popped up to fill the voidness.
Rails was also one of the firsts that provided a solution. It introduced try
function to check whether an indexing returns null or not.
Rails introduced try
to achieve that functionality.
The above code looks like as follows:
people.each do |person|
p person.try(:address).try(:street)
end
# => 'somewhere over the rainbow'
# => nil
So we have got some insight how to handle embedded objects and avoid raising NoMethodError
exceptions.
Code can be found: https://github.com/torokmark/safe-navigation-in-languages