Ruby - Access multidimensional hash and avoid access nil object
Solution 1
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
You could also use Object#andand.
my_hash['key1'].andand['key2'].andand['key3']
Solution 3
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.
2) 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
3) If you feel monadic you can take a look at this, Maybe
![Nobita](https://i.stack.imgur.com/nKfI6.jpg?s=256&g=1)
Comments
-
Nobita about 4 years
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?
-
Piotr Zolnierek about 12 yearsYou can also try the monadic gem, which has a Maybe (and other monads) which help with handling exceptions
-
jakeonrails about 11 yearsWhat are your thoughts on using
rescue nil
at the end of the statement? -
dbenhur about 11 years@jakeonrails
rescue nil
is almost always evil. 1) it can capture and silently discard an exception you weren't aware could be thrown; 2) exceptions are computationally expensive flow control -- one should only use them for exceptional behavior, not expected behavior. -
LYu over 7 yearsYou can use
dig
method for hash after Ruby 2.3, ruby-doc.org/core-2.3.0_preview1/Hash.html#method-i-dig -
sites over 7 years
-
azerty over 7 yearsYou can use dig method for hash and Array since Ruby 2.3
-
thisismydesign almost 7 years
my_hash.dig
will fail ifmy_hash
isnil
. Consider using the safe navigation operator instead or combined with.dig
as:my_hash&.dig(:key1, :key2, :key3)
ormy_hash&.key1&.key2&.key3
. -
thisismydesign almost 7 yearsThe syntax of the safe navigator operator on hashes in my previous comment is incorrect. The correct syntax is:
my_hash&.[]('key1')&.[]('key2')&.[]('key3')
.