Reputation: 1732
I have the following small sequence, which makes no sense to me:
irb(main):001:0> h = {}
=> {}
irb(main):002:0> h.default = {}
=> {}
irb(main):003:0> h["foo"]["bar"] = 6
=> 6
irb(main):004:0> h.length
=> 0
irb(main):005:0> h.keys
=> []
irb(main):006:0> h["foo"]
=> {"bar"=>6}
How is is that step 5 returns an empty list of keys, and step 4 indicates the length of h
is 0
, yet I can see in step 6 that "foo"
is a valid key and has an associated value. I would expect keys
to return ["foo"]
, and length
to return 1
.
What am I misunderstanding? Note this is Ruby 1.9.3p0
Also note this works correctly:
irb(main):001:0> h = {}
=> {}
irb(main):002:0> h["foo"] = {}
=> {}
irb(main):003:0> h["foo"]["bar"] = 6
=> 6
irb(main):004:0> h.length
=> 1
irb(main):005:0> h.keys
=> ["foo"]
irb(main):006:0> h["foo"]
=> {"bar"=>6}
The only difference is the use of Hash.default to set the default value and skip the explicit initialization of h["foo"]
. Is this a bug?
Upvotes: 0
Views: 119
Reputation: 160631
Meditate on this:
h={} #=> {}
h['foo'] #=> nil
Because ['foo']
doesn't exist trying to access it results in a nil. That means that trying to access a sub-hash of 'foo'
would fail:
h['foo']['bar']
NoMethodError: undefined method `[]' for nil:NilClass
That'd be the equivalent of using:
nil['bar']
NoMethodError: undefined method `[]' for nil:NilClass
We can fix this by defining what h['foo']
is:
h['foo'] = {} #=> {}
It's defined, so when we request its value we get an empty hash:
h['foo'] #=> {}
And now h['foo']['bar']
will return something expected, a nil:
h['foo']['bar'] # => nil
And, at that point an assignment to h['foo']['bar']
will make sense:
h['foo']['bar'] = 6
And looking at h
shows we've got a hash of hashes:
h # => {"foo"=>{"bar"=>6}}
Using h.default = {}
is an attempt to work around the problem of h['foo']
returning nil:
h = {}
h.default = {}
h # => {}
h['foo'] # => {}
And, yes, it does that, but it also introduces a MAJOR problem. We expect hashes to return the value of the key consistently, but h.default = {}
has set up a problem. Normally we want a hash to behave like this:
h = {}
h # => {}
h['foo'] = {}
h['bar'] = {}
h # => {"foo"=>{}, "bar"=>{}}
h['foo']['baz'] = 1
h['bar']['baz'] = 2
h # => {"foo"=>{"baz"=>1}, "bar"=>{"baz"=>2}}
h['foo'].object_id # => 70179045516300
h['bar'].object_id # => 70179045516280
h['foo']['baz'].object_id # => 3
h['bar']['baz'].object_id # => 5
Using h.default = {}
breaks that. We knew h['foo']
existed and we'd expect that h['bar']
would be non-existent or point to a different key/value pair in memory, but it won't:
h = {}
h.default = {}
h # => {}
h['foo']['baz'] = 1
h['bar']['baz'] = 2
h # => {}
h['foo'].object_id # => 70142994238340
h['bar'].object_id # => 70142994238340
h['foo']['baz'] # => 2
h['bar']['baz'] # => 2
h['foo']['baz'].object_id # => 5
h['bar']['baz'].object_id # => 5
You really don't want that to happen as it's totally alien and unexpected behavior, which would make your code break in weird and wonderful ways, plus make you hated by anyone else maintaining/debugging your code.
The whole problem is the use of h.default = {}
because it doesn't mean "always use a new hash" it means "always use this hash".
Instead, something like this would work and fix the problem:
h = Hash.new{ |hash,key| hash[key] = Hash.new(&hash.default_proc) }
h['foo']['baz'] = 1
h['bar']['baz'] = 2
h # => {"foo"=>{"baz"=>1}, "bar"=>{"baz"=>2}}
h['foo'].object_id # => 70109399098340
h['bar'].object_id # => 70109399098320
h['foo']['baz'] # => 1
h['bar']['baz'] # => 2
h['foo']['baz'].object_id # => 3
h['bar']['baz'].object_id # => 5
At this point, the hash should behave as expected and nobody would want to hunt you down. There are some gotchas to this solution but it's a better building block. See "Dynamically creating a multi-dimensional hash in Ruby" for more information.
Upvotes: 0
Reputation: 36110
You just set the default value that will be returned if no value is found. This doesn't change the fact that there is no value assigned to h["foo"]
. {"bar"=>6}
will be the value for any key which is not found.
h = {}
h.default = {} # => {}
h["foo"]["bar"] = 6 # => 6
h["foo"] # => {"bar"=>6}
h["baz"] # => {"bar"=>6}
If you wanted a hash which returns and sets values of missing keys to empty hashes, you have to do:
h = Hash.new { |hash, key| hash[key] = {} }
h["foo"]["bar"] = 6 # => 6
h # => {:foo=>{:bar=>6}}
Upvotes: 1
Reputation: 10738
The line h.default = {}
means that the default value that is returned in case the key is missing is {}
. Moreover, it is this particular hash instance that will be returned.
The line h["foo"]["bar"] = 6
looks for the key foo
and when it can't find it, it fetches the default value {}
and inserts into it the key bar
and value 6
. The hash is still empty after this.
This is the reason why you see these results.
You may use default_proc to also set the key.
Upvotes: 2