Ruby - Access multidimensional hash and avoid access nil object

RubyHashConditional

Ruby Problem Overview


> Possible Duplicate:
> Ruby: Nils in an IF statement
> Is there a clean way to avoid calling a method on nil in a nested params hash?

Let's say I try to access a hash like this:

my_hash['key1']['key2']['key3']

This is nice if key1, key2 and key3 exist in the hash(es), but what if, for example key1 doesn't exist?

Then I would get NoMethodError: undefined method [] for nil:NilClass. And nobody likes that.

So far I deal with this doing a conditional like:

if my_hash['key1'] && my_hash['key1']['key2'] ...

Is this appropriate, is there any other Rubiest way of doing so?

Ruby Solutions


Solution 1 - Ruby

There are many approaches to this.

If you use Ruby 2.3 or above, you can use dig

my_hash.dig('key1', 'key2', 'key3')

Plenty of folks stick to plain ruby and chain the && guard tests.

You could use stdlib Hash#fetch too:

my_hash.fetch('key1', {}).fetch('key2', {}).fetch('key3', nil)

Some like chaining ActiveSupport's #try method.

my_hash.try(:[], 'key1').try(:[], 'key2').try(:[], 'key3')

Others use andand

myhash['key1'].andand['key2'].andand['key3']

Some people think egocentric nils are a good idea (though someone might hunt you down and torture you if they found you do this).

class NilClass
  def method_missing(*args); nil; end
end

my_hash['key1']['key2']['key3']

You could use Enumerable#reduce (or alias inject).

['key1','key2','key3'].reduce(my_hash) {|m,k| m && m[k] }

Or perhaps extend Hash or just your target hash object with a nested lookup method

module NestedHashLookup
  def nest *keys
    keys.reduce(self) {|m,k| m && m[k] }
  end
end

my_hash.extend(NestedHashLookup)
my_hash.nest 'key1', 'key2', 'key3'

Oh, and how could we forget the maybe monad?

Maybe.new(my_hash)['key1']['key2']['key3']

Solution 2 - Ruby

You could also use Object#andand.

my_hash['key1'].andand['key2'].andand['key3']

Solution 3 - Ruby

Conditions my_hash['key1'] && my_hash['key1']['key2'] don't feel DRY.

Alternatives:

  1. autovivification magic. From that post:

    def autovivifying_hash Hash.new {|ht,k| ht[k] = autovivifying_hash} end

Then, with your example:

my_hash = autovivifying_hash     
my_hash['key1']['key2']['key3']

It's similar to the Hash.fetch approach in that both operate with new hashes as default values, but this moves details to the creation time. Admittedly, this is a bit of cheating: it will never return 'nil' just an empty hash, which is created on the fly. Depending on your use case, this could be wasteful.

  1. Abstract away the data structure with its lookup mechanism, and handle the non-found case behind the scenes. A simplistic example:

    def lookup(model, key, *rest) v = model[key] if rest.empty? v else v && lookup(v, *rest) end end

    lookup(my_hash, 'key1', 'key2', 'key3') => nil or value

  2. If you feel monadic you can take a look at this, Maybe

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionNobitaView Question on Stackoverflow
Solution 1 - RubydbenhurView Answer on Stackoverflow
Solution 2 - RubySergio TulentsevView Answer on Stackoverflow
Solution 3 - RubyingerView Answer on Stackoverflow