This question already has an answer here:
I'm working a little utility written in ruby that makes extensive use of nested hashes. Currently, I'm checking access to nested hash elements as follows:
structure = { :a => { :b => 'foo' }}
# I want structure[:a][:b]
value = nil
if structure.has_key?(:a) && structure[:a].has_key?(:b) then
value = structure[:a][:b]
end
Is there a better way to do this? I'd like to be able to say:
value = structure[:a][:b]
And get nil
if :a is not a key in structure
, etc.
Solution 1
I suggested this in my question before:
Hash#to_hash
is already defined, and returns self. Then you can do:The
to_hash
ensures that you get an empty hash when the previous key search fails.Solution2
This solution is similar in spirit to mu is too short's answer in that it uses a subclass, but still somewhat different. In case there is no value for a certain key, it does not use a default value, but rather creates a value of empty hash, so that it does not have the problem of confusion in assigment that DigitalRoss's answer has, as was pointed out by mu is too short.
It departs from the specification given in the question, though. When an undefined key is given, it will return an empty hash instread of
nil
.If you build an instance of this NilFreeHash from the beginning and assign the key-values, it will work, but if you want to convert a hash into an instance of this class, that may be a problem.
The way I usually do this these days is:
This will give you a hash that creates a new hash as the entry for a missing key, but returns nil for the second level of key:
You can nest this to add multiple layers that can be addressed this way:
You can also chain indefinitely by using the
default_proc
method:The above code creates a hash whose default proc creates a new Hash with the same default proc. So, a hash created as a default value when a lookup for an unseen key occurs will have the same default behavior.
EDIT: More details
Ruby hashes allow you to control how default values are created when a lookup occurs for a new key. When specified, this behavior is encapsulated as a
Proc
object and is reachable via thedefault_proc
anddefault_proc=
methods. The default proc can also be specified by passing a block toHash.new
.Let's break this code down a little. This is not idiomatic ruby, but it's easier to break it out into multiple lines:
Line 1 declares a variable
recursive_hash
to be a newHash
and begins a block to berecursive_hash
'sdefault_proc
. The block is passed two objects:h
, which is theHash
instance the key lookup is being performed on, andk
, the key being looked up.Line 2 sets the default value in the hash to a new
Hash
instance. The default behavior for this hash is supplied by passing aProc
created from thedefault_proc
of the hash the lookup is occurring in; ie, the default proc the block itself is defining.Here's an example from an IRB session:
When the hash at
recursive_hash[:foo]
was created, itsdefault_proc
was supplied byrecursive_hash
'sdefault_proc
. This has two effects:recursive_hash[:foo]
is the same asrecursive_hash
.recursive_hash[:foo]
'sdefault_proc
will be the same asrecursive_hash
.So, continuing in IRB, we get the following:
I am currently trying out this:
I know the arguments against
try
, but it is useful when looking into things, like say,params
.There is the cute but wrong way to do this. Which is to monkey-patch
NilClass
to add a[]
method that returnsnil
. I say it is the wrong approach because you have no idea what other software may have made a different version, or what behavior change in a future version of Ruby can be broken by this.A better approach is to create a new object that works a lot like
nil
but supports this behavior. Make this new object the default return of your hashes. And then it will just work.Alternately you can create a simple "nested lookup" function that you pass the hash and the keys to, which traverses the hashes in order, breaking out when it can.
I would personally prefer one of the latter two approaches. Though I think it would be cute if the first was integrated into the Ruby language. (But monkey-patching is a bad idea. Don't do that. Particularly not to demonstrate what a cool hacker you are.)
You can use the andand gem, but I'm becoming more and more wary of it: