Ruby - Access multidimensional hash and avoid access nil object
RubyHashConditionalRuby 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:
-
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.
-
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
-
If you feel monadic you can take a look at this, Maybe