Accessing elements of nested hashes in ruby

99,657

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:

  1. The default behavior for recursive_hash[:foo] is the same as recursive_hash.
  2. The default behavior for hashes created by recursive_hash[:foo]'s default_proc will be the same as recursive_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
Share:
99,657

Related videos on Youtube

Paul Morie
Author by

Paul Morie

Updated on August 12, 2021

Comments

  • Paul Morie
    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 in structure, etc.

    • user513951
      user513951 over 8 years
      Ruby 2.3 added Hash#dig to solve exactly this problem. See my answer below.
  • Phrogz
    Phrogz about 13 years
    Aww, I've never been called cute before. :) I do agree that this is convenient but ultimately too dangerous to use.
  • Wayne Conrad
    Wayne Conrad about 13 years
    This 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
    Phrogz about 13 years
    I 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
    Wayne Conrad about 13 years
    Michael, Is the reason for your wariness worth mentioning?
  • fl00r
    fl00r about 13 years
    Thanx, @Wayne. Good explanation
  • Michael Kohl
    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
    John F. Miller about 13 years
    Another 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
    mu is too short about 13 years
    Aren't you introducing a bit of reference problem with Hash.new({})? Won't every default entry end up using the same hash?
  • DigitalRoss
    DigitalRoss about 13 years
    Yes, 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
    mu is too short about 13 years
    But 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
    PJP about 13 years
    I'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
    PJP about 13 years
    Why not combine both your answers into one?
  • sawa
    sawa about 13 years
    They 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
    PJP about 13 years
    Put them both in one answer. That's a very common practice on SO, especially when you compare/contrast them.
  • PJP
    PJP about 13 years
    Cool. 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
    sawa about 13 years
    I see. I guess I am still learning know about the custom here.
  • DigitalRoss
    DigitalRoss about 13 years
    That 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
    rainkinz over 10 years
    Why 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
    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 the find method, your version doesn't have anywhere to return from so Ruby throws a hissy fit.
  • rainkinz
    rainkinz over 10 years
    Ah, of course. Thanks for explanation.
  • Dorian
    Dorian over 7 years
    For those interested, here is the source code of xkeys: gist.github.com/Dorian/d5330b42473f15c1019b26dd3519eaca
  • Josh
    Josh about 7 years
    this should be the accepted answer
  • user513951
    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
    Brian K about 3 years

Related