Setting ruby hash .default to a list

16,454

Solution 1

Hash.default is used to set the default value returned when you query a key that doesn't exist. An entry in the collection is not created for you, just because queried it.

Also, the value you set default to is an instance of an object (an Array in your case), so when this is returned, it can be manipulated.

a = {}
a.default = []     # set default to a new empty Array
a[8] << 9          # a[8] doesn't exist, so the Array instance is returned, and 9 appended to it
a.default          # => [9]
a[9]               # a[9] doesn't exist, so default is returned

Solution 2

This is a very useful idiom:

(myhash[key] ||= []) << value

It can even be nested:

((myhash[key1] ||= {})[key2] ||= []) << value

The other way is to do:

myhash = Hash.new {|hash,key| hash[key] = []}

But this has the significant side-effect that asking about a key will create it, which renders has_key? fairly useless, so I avoid this method.

Solution 3

I think this is the behavior you are looking for. This will automatically initialize any new keys in the Hash to an array:

irb(main):001:0> h = Hash.new{|h, k| h[k] = []}
=> {}
irb(main):002:0> h[1] << "ABC"
=> ["ABC"]
irb(main):003:0> h[3]
=> []
irb(main):004:0> h
=> {1=>["ABC"], 3=>[]}

Solution 4

glenn mcdonald says:

"The other way is to do:

myhash = Hash.new {|hash,key| hash[key] = []}

But this has the significant side-effect that asking about a key will create it, which renders has_key? fairly useless, so I avoid this method."

that does not in fact seem to be true.

irb(main):004:0> a = Hash.new {|hash,key| hash[key] = []}
=> {}
irb(main):005:0> a.has_key?(:key)
=> false
irb(main):006:0> a[:key]
=> []
irb(main):007:0> a.has_key?(:key)
=> true

Accessing the key will create it, as I would expect. Merely asking has_key? does not.

Solution 5

If you really wanna have an endlessly deep hash:

endless = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
endless["deep"]["in"]["here"] = "hello"

Of course, as Glenn points out above, if you do this, the has_key? looses its meaning as it will always return true. Thx to jbarnette for this one.

Share:
16,454
mat kelcey
Author by

mat kelcey

Updated on June 06, 2022

Comments

  • mat kelcey
    mat kelcey about 2 years

    I thought I understood what the default method does to a hash...

    Give a default value for a key if it doesn't exist:

    irb(main):001:0> a = {}
    => {}
    irb(main):002:0> a.default = 4
    => 4
    irb(main):003:0> a[8]
    => 4
    irb(main):004:0> a[9] += 1
    => 5
    irb(main):005:0> a
    => {9=>5}
    

    All good.

    But if I set the default to be a empty list, or empty hash, I don't understand it's behaviour at all....

    irb(main):001:0> a = {}
    => {}
    irb(main):002:0> a.default = []
    => []
    irb(main):003:0> a[8] << 9
    => [9]                          # great!
    irb(main):004:0> a
    => {}                           # ?! would have expected {8=>[9]}
    irb(main):005:0> a[8]
    => [9]                          # awesome!
    irb(main):006:0> a[9]
    => [9]                          # unawesome! shouldn't this be [] ??
    

    I was hoping/expecting the same behaviour as if I had used the ||= operator...

    irb(main):001:0> a = {}
    => {}
    irb(main):002:0> a[8] ||= []
    => []
    irb(main):003:0> a[8] << 9
    => [9]
    irb(main):004:0> a
    => {8=>[9]}
    irb(main):005:0> a[9]
    => nil
    

    Can anyone explain what is going on?

  • mat kelcey
    mat kelcey over 15 years
    good explanation, makes sense
  • Chris Lowis
    Chris Lowis over 12 years
    I don't think sure the side-effect of the last technique is present in ruby 1.9.2. myhash = Hash.new {|hash,key| hash[key] = []}; myhash.has_key?(:test) #=> false
  • mashe
    mashe over 12 years
    Oh, no, I meant that puts myhash[:test] or the like, which seems like it should be harmless, will now result in myhash.has_key?(:test) being true afterwards.
  • Stumpy Joe Pete
    Stumpy Joe Pete over 11 years
    I'd like to point out that this behavior is different from python's defaultdict, where the analogous code works just fine.
  • Steeve McCauley
    Steeve McCauley over 10 years
    Wow, this is horrible, and terribly prone to bugs by dynamically changing this default value. I just spent 3 hours trying to figure out wtf was going on when attempting to set default to []. IMNSHO, default should not be exposed like this. The alternative (h=Hash.new{|h,k| h[k]=[]}) doesn't really do what I want either because that always creates a new array by just referencing the hash key. Now I'm back to doing what I was trying to avoid in the first place: h[k]=[] unless h.has_key?(k) :)
  • Halil Özgür
    Halil Özgür over 10 years
    I hope you guys didn't down-vote this answer just for that peculiarity of Ruby itself :) What's wrong with this excellently explanatory answer? I myself hadn't completely gotten my head around the concept until I saw the emphasis on "returned" and the explanation on the importance of the value of default being an object (so that it's kept around in subsequent non-existent key calls).