5

I have a Hash and I want to insert some data into it at a deep level, but a key might be missing at any level. So, I am conditionally initializing it before updating its value at every level.

What would be a better way to write this or an approach that can make code less ugly?

data[:foo] ||= {}
data[:foo][:bar] ||= {}
data[:foo][:bar][:baz] ||= []
data[:foo][:bar][:baz] << 99
sawa
  • 165,429
  • 45
  • 277
  • 381
Jagdeep Singh
  • 4,880
  • 2
  • 17
  • 22

3 Answers3

10

Use hash autovivification:

data = Hash.new { |h, k| h[k] = h.dup.clear }
#⇒ {}

# or, credits to @Amadan:
data = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
#⇒ {}

data[:foo][:bar][:baz] = 42
data
#⇒ {:foo=>{:bar=>{:baz=>42}}}

The trick used here is we use Hash#default_proc to create nested keys.

For your case:

(data[:foo][:bar][:baz] = []) << 99
Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160
  • data[:foo][:bar][:baz] ||= [] => {:foo=>{:bar=>{:baz=>{}}}} – Bryan Ash May 22 '18 at 12:59
  • @BryanAsh so what? One obviously should not call `||=` on the hash with a `default_proc`, use `=` instead. – Aleksei Matiushkin May 22 '18 at 13:01
  • 5
    `Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }` is a bit more efficient. – Amadan May 22 '18 at 13:16
  • 4
    Eh, credits, as if I invented it :D anyway, it's a known incantation, with a somewhat less known name: "hash autovivification". – Amadan May 22 '18 at 13:20
  • 1
    I suppose that if `data` is a given non-empty hash one simply attaches a default proc. If one does so could there be side-effects? For example, what if the hash already had a default proc? Note that it would be necessary to use `h.key?(k)` to check if a key exists, rather than relying on `h[k].nil?`. – Cary Swoveland May 22 '18 at 20:40
  • @mudasobwa perfect answer to the question. Kudos to Amadan too for more efficient approach. Just a note: It will set the keys in hash even if we just access `data[:foo][:bar][:baz]` before setting its value. Not a problem for me, but good to remember. – Jagdeep Singh May 23 '18 at 05:22
1

One could use recursion.

def stuff_it(h, first_key, *rest_keys, val)
  if rest_keys.empty?
    (h[first_key] ||= []) << val
  else
    h[first_key] = stuff_it(h[first_key] ||= {}, *rest_keys, val)
  end
  h
end   

stuff_it({ a: 1 }, :foo, :bar, :baz, 99)
  #=> {:a=>1, :foo=>{:bar=>{:baz=>[99]}}}
stuff_it({ a: 1, foo: { b: 2 } }, :foo, :bar, :baz, 99)
  #=> {:a=>1, :foo=>{:b=>2, :bar=>{:baz=>[99]}}}
stuff_it({ a: 1, foo: { b: 2, bar: { c: 3 } } }, :foo, :bar, :baz, 99)
  #=> {:a=>1, :foo=>{:b=>2, :bar=>{:c=>3, :baz=>[99]}}}
h = { a: 1, foo: { b: 2, bar: { c: 3, baz: [88] } } }
stuff_it(h, :foo, :bar, :baz, 99)
  #=> {:a=>1, :foo=>{:b=>2, :bar=>{:c=>3, :baz=>[88, 99]}}}
h # => {:a=>1, :foo=>{:b=>2, :bar=>{:c=>3, :baz=>[88, 99]}}}

As one can see from the last example, the method is destructive. It can be made non-destructive by making a small change.

def stuff_it(g, first_key, *rest_keys, val)
  h = g.merge(g)
  if rest_keys.empty?
     h[first_key] = h[first_key] ? h[first_key].dup << val : [val]
  else
    h[first_key] = stuff_it(h[first_key] ||= {}, *rest_keys, val)
  end
  h
end   

h = { a: 1, foo: { b: 2, bar: { c: 3 } } }
stuff_it(h, :foo, :bar, :baz, 99)
  #=> {:a=>1, :foo=>{:b=>2, :bar=>{:c=>3, :baz=>[99]}}}
h #=> { a: 1, foo: { b: 2, bar: { c: 3 } } }

h = { a: 1, foo: { b: 2, bar: { c: 3, baz: [88] } } }
stuff_it(h, :foo, :bar, :baz, 99)
  #=> {:a=>1, :foo=>{:b=>2, :bar=>{:c=>3, :baz=>[88, 99]}}}
h #=> {:a=>1, :foo=>{:b=>2, :bar=>{:c=>3, :baz=>[88]}}}
Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
0

You could also do something like:

class SpecialHash < Hash
  def [](key)
    if has_key?(key)
      super(key)
    else
      self[key] = self.class.new
    end
  end
end

h = SpecialHash.new
h[:foo][:bar][:baz] = "Baz"
h # => {:foo=>{:bar=>{:baz=>"Baz"}}}

It duck types exactly the same as a Hash.

You can reformat the same code as:

class SpecialHash < Hash
  def [](key)
    return super if has_key?(key)
    self[key] = self.class.new
  end
end

or even

class SpecialHash < Hash
  def [](key)
    has_key?(key) ? super : self[key] = self.class.new
  end
end
Kris
  • 19,188
  • 9
  • 91
  • 111