Accessing elements of nested hashes in ruby
Solution 1
The way I usually do this these days is:
h = Hash.new { |h,k| h[k] = {} }
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:
h['foo'] -> {}
h['foo']['bar'] -> nil
You can nest this to add multiple layers that can be addressed this way:
h = Hash.new { |h, k| h[k] = Hash.new { |hh, kk| hh[kk] = {} } }
h['bar'] -> {}
h['tar']['zar'] -> {}
h['scar']['far']['mar'] -> nil
You can also chain indefinitely by using the default_proc
method:
h = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
h['bar'] -> {}
h['tar']['star']['par'] -> {}
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 the default_proc
and default_proc=
methods. The default proc can also be specified by passing a block to Hash.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:
1. recursive_hash = Hash.new do |h, k|
2. h[k] = Hash.new(&h.default_proc)
3. end
Line 1 declares a variable recursive_hash
to be a new Hash
and begins a block to be recursive_hash
's default_proc
. The block is passed two objects: h
, which is the Hash
instance the key lookup is being performed on, and k
, 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 a Proc
created from the default_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:
irb(main):011:0> recursive_hash = Hash.new do |h,k|
irb(main):012:1* h[k] = Hash.new(&h.default_proc)
irb(main):013:1> end
=> {}
irb(main):014:0> recursive_hash[:foo]
=> {}
irb(main):015:0> recursive_hash
=> {:foo=>{}}
When the hash at recursive_hash[:foo]
was created, its default_proc
was supplied by recursive_hash
's default_proc
. This has two effects:
- The default behavior for
recursive_hash[:foo]
is the same asrecursive_hash
. - The default behavior for hashes created by
recursive_hash[:foo]
'sdefault_proc
will be the same asrecursive_hash
.
So, continuing in IRB, we get the following:
irb(main):016:0> recursive_hash[:foo][:bar]
=> {}
irb(main):017:0> recursive_hash
=> {:foo=>{:bar=>{}}}
irb(main):018:0> recursive_hash[:foo][:bar][:zap]
=> {}
irb(main):019:0> recursive_hash
=> {:foo=>{:bar=>{:zap=>{}}}}
Solution 2
Traditionally, you really had to do something like this:
structure[:a] && structure[:a][:b]
However, Ruby 2.3 added a method Hash#dig
that makes this way more graceful:
structure.dig :a, :b # nil if it misses anywhere along the way
There is a gem called ruby_dig
that will back-patch this for you.
Solution 3
Hash
and Array
have a method called dig
that solves this problem entirely.
value = structure.dig(:a, :b)
It returns nil
if the key is missing at any level.
If you are using a version of Ruby older than 2.3, you can use the ruby_dig
gem.
Solution 4
I made rubygem for this. Try vine.
Install:
gem install vine
Usage:
hash.access("a.b.c")
Solution 5
I think one of the most readable solutions is using Hashie:
require 'hashie'
myhash = Hashie::Mash.new({foo: {bar: "blah" }})
myhash.foo.bar
=> "blah"
myhash.foo?
=> true
# use "underscore dot" for multi-level testing
myhash.foo_.bar?
=> true
myhash.foo_.huh_.what?
=> false
Related videos on Youtube
Paul Morie
Updated on August 12, 2021Comments
-
Paul Morie over 2 years
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 instructure
, etc.-
user513951 over 8 yearsRuby 2.3 added
Hash#dig
to solve exactly this problem. See my answer below.
-
-
Phrogz about 13 yearsAww, I've never been called cute before. :) I do agree that this is convenient but ultimately too dangerous to use.
-
Wayne Conrad about 13 yearsThis will silently turn missing variables or methods, among other things, into nil. In a way that's the intent, but this is a broad knife for what perhaps should be a fine cut.
-
Phrogz about 13 yearsI think you should remove the bit about the top-level hash, since this won't apply arbitrarily deep, so
h[:foo][:bar][:jim]
will still blow up. -
Wayne Conrad about 13 yearsMichael, Is the reason for your wariness worth mentioning?
-
fl00r about 13 yearsThanx, @Wayne. Good explanation
-
Michael Kohl about 13 years@Wayne: I've found that spurious use of
andand
in our app sometimes covers a problem that could have been found easily for too long. -
John F. Miller about 13 yearsAnother gotcha with the default hash value is that it is not persistable. If you dump your data structure to disk and load it later it will loose it's default state, same thing if you send it over the wire. Unlike other classes that explicitly raise errors when this happens Marshal.dump(Hash.new(foo)) will succeed happily loosing your default value.
-
mu is too short about 13 yearsAren't you introducing a bit of reference problem with
Hash.new({})
? Won't every default entry end up using the same hash? -
DigitalRoss about 13 yearsYes, every default will use the same empty hash, but that's OK.
h[:new_key] = new_value
will create a new entry, and won't modify the default value. -
mu is too short about 13 yearsBut if you start doing things like
h = Hash.new({}); h[:a][:b] = 1; h[:c][:d] = 2
you're in for a mess of confusion. -
PJP about 13 yearsI'm very cautious about using
rescue
like that, much for the same reason that Wayne gives, but also because it can mask errors in logic or syntax that you should know about. Finding errors masked this way can be tough. -
PJP about 13 yearsWhy not combine both your answers into one?
-
sawa about 13 yearsThey are unrelated, and I though it's better for them to be separated. Are you suggesting to put two solutions in one post, or combining them to give a different answer?
-
PJP about 13 yearsPut them both in one answer. That's a very common practice on SO, especially when you compare/contrast them.
-
PJP about 13 yearsCool. I find it nice that way because it's a lot easier to see the differences in the approaches. That's a lot harder when they're separate answers.
-
sawa about 13 yearsI see. I guess I am still learning know about the custom here.
-
DigitalRoss about 13 yearsThat would be bad, si. It gives you multi-level reads but takes away multi-level writes in most cases. It's perhaps a bit too dangerous as a general idea.
-
rainkinz over 10 yearsWhy don't you get a local jump error from that return? Your code definitely works. I would think this is roughly equivalent: >> hash = {:a => {:b => 'k'}} >> [:a, :b].inject(hash) {|h, x| return nil if (h[x].nil? || !h[x].is_a?(Hash)); h[x] }, but gives a LocalJumpError
-
mu is too short over 10 years@rainkinz: You're getting a LocalJumpError because your block is trying to return without being inside a method (or "a method which declared the block" to be pedantic). My
return
works because it is returning from thefind
method, your version doesn't have anywhere to return from so Ruby throws a hissy fit. -
rainkinz over 10 yearsAh, of course. Thanks for explanation.
-
Dorian over 7 yearsFor those interested, here is the source code of
xkeys
: gist.github.com/Dorian/d5330b42473f15c1019b26dd3519eaca -
Josh about 7 yearsthis should be the accepted answer
-
user513951 over 6 years@Josh FYI the reason it is not is that this answer was edited to add the correct answer at a much later date
-
Brian K about 3 years