When building applications in Ruby, almost every developer works with hashes on a daily basis. We especially run into nested hashes all the time, for example:
u = { 'user' => { 'id' => 1, 'lang' => { 'id' => 1, 'name' => 'Ruby' } } }
u['user']['lang']['name'] # => "Ruby"
Chaining keys like this looks cool, but it only works as long as every key actually holds a value:
u = { 'user' => { 'id' => 2, 'lang' => nil } }
u['user']['lang']['name'] # undefined method '[]' for nil (NoMethodError)
To fix this error, you could use the send method (though it looks pretty terrifying):
u.send(:[], 'user')&.send(:[], 'lang')&.send(:[], 'name') || 'No name'
Or a much more elegant solution using dig:
u.dig('user', 'lang', 'name') || 'No name'
By the way, in case you didn’t know, dig can also be combined with arrays:
u = { 'user' => { 'id' => 2, 'roles' => [{ 'id' => 1, 'name' => 'admin' }] } }
u.dig('user', 'roles', 0, 'name') # => "admin"
But what if you need a fallback value for a key that isn’t present in the hash or if the hash itself might be nil? Over my years of working with Ruby, I’ve landed on this approach:
h = [
{ 'key' => 'value' },
{},
nil
].sample
# Rubocop complains about this when enabled Style/SingleArgumentDig
h&.dig('otherkey') || 'None'
# Looks terrible (I don't think this is a suitable case for the send method)
h&.send(:[], 'otherkey') || 'None'
# It's better, but duplicating the default value looks disgusting
h&.fetch('otherkey', 'None') || 'None'
# Same as the example above, only the default value is in the block
h&.fetch('otherkey') { 'None' } || 'None'
# So far this is the best option of all
h&.fetch('otherkey', nil) || 'None'
# Unfortunately, we can't use it this way
# because if the key is missing, an error will occur.
h&.fetch('otherkey') || 'None'
Have you encountered this in your work? Is there a more elegant solution that I might have missed?
P.S.
I also recommend reading this article.