The objective
Let's start here:
h = {}
h[:a] = [] unless h.key?(:a)
h[:a] << 7
We create an empty hash h
. Each value of this hash will be an array of values. We wish to append 7
to the array that is the value of key :a
. If h
already has a key :a
, we have something like h[:a] #=> [3,5]
, so we just append 7
to that array:
h[:a] << 7 #=> [3,5,7]
If, however, h
does not have a key :a
(so h[:a] #=> nil
), we first need to set h[:a]
to an empty array. Hence the need for:
h[:a] = [] unless h.key?(:a)
If h
cannot already have a key :a
with value nil
(in other words, if no values are intentionally nil
), we could instead write:
h[:a] = [] unless h[:a]
Ruby-like trick
We can make this more Ruby-like in a couple of ways. The first is:
h = {}
(h[:a] ||= []) << 7 #=> [7]
h #=> {:a=>[7]}
(h[:a] ||= []) << 9 #=> [7, 9]
h #=> {:a=>[7, 9]}
When Ruby sees:
h[:a] ||= []
the first thing she does it convert it to:
h[:a] = h[:a] || []
The steps are therefore as follows:
(h[:a] = h[:a] || []) << 7
#=> (h[:a] = nil || []) << 7
#=> (h[:a] = []) << 7
#=> h[:a] << 7
h[:a] #=> [7]
(h[:a] = h[:a] || []) << 9
#=> (h[:a] = [7] || []) << 9
#=> (h[:a] = [7]) << 9
#=> h[:a] << 9
h[:a] #=> [7,9]
Ruby-like default value for hash
The second more Ruby-like way is equivalent to the first, but implemented by defining a default value for the hash. If a hash h
does not have a key k
, h[k]
returns the default value. Does this also change the hash? Let's put off the question for the moment.
The docs for Hash::new explain that there are two ways to define a default value.
Wrong default value
The first, for making the default value an empty array, is:
h = Hash.new([]) #=> {}
so if we now write:
h[:a] #=> []
this returns the default value ([]
). It does not alter the hash:
h #=> {}
We may be tempted to write:
h[:a] << 7
#=> [] << 7 => [7]
(h[:a]
returns the default value because there is no key :a
.) As you see, this does not alter the hash, it just returns an array [7]
which is not attached to anything, so is garbage-collected.
Now let's try this:
(h[:a] = h[:a]) << 7
#=> (h[:a] = []) << 7 => [7]
h #=> {:a=>[7]}
(h[:a] = h[:a]) << 9
#=> (h[:a] = [7]) << 9 => [7, 9]
h #=> {:a=>[7, 9]}
So far, so good, but there's a problem:
(h[:b] = h[:b]) << 11 #=> [7, 9, 11]
h #=> {:a=>[7, 9, 11], :b=>[7, 9, 11]}
(h[:b] = h[:b]) << 13 #=> [7, 9, 11, 13]
h #=> {:a=>[7, 9, 11, 13], :b=>[7, 9, 11, 13]}
As you see, there is just one default empty array.
Right default value
Fortunately, there is another way to specify a default array: by giving Hash#new
a block. (We've finally reached the point of the question.)
If the hash h
does not have a key k
, h[k]
invokes the block. The block has two block variables, h
and k
. You can do whatever you want in the block. For example:
@a = 3
h = Hash.new { |h,k| @a = 7 }
h[:a] #=> 7
h #=> {}
@a #=> 7
Now I doubt very much that you'd want to do this, but you could. For the present problem you want the value of a new key to be an empty array:
h = Hash.new { |h,k| h[k] = [] }
(Be careful with this. If we executed:
puts h[:a]
#=> []
we'd then have
h #=> { :a=>[] }
)
So:
h[:a] << 7
first invokes the block, because h
does not have a key k
, so we now have:
{ :a=>[] }[:a] << 7
h #=> { :a=>[7] }
Now when we execute:
h[:a] << 9
h #=> {:a=>[7, 9]}
the block is not called, because h
has a key :a
.
That's about it. Any questions?