3

I am new to Ruby and running Ruby Koans. In Ruby Koans, in about_hashes.rb file, there is an example of assigning default value to a hash.

hash = Hash.new([])
hash[:one] << "uno"
hash[:two] << "dos"

puts hash[:one]  # this is  ["uno", "dos"]

here both hash[:one] & hash[:two] or any key like hash[:three] (non existing key) all have the value ["uno", and "dos"] I did not understand how "<<" is used here. Also, when I tried extracting keys & values of the hash, or print the keys/values, it is empty.

puts (hash.values.size)  # size is 0 here
puts (hash.keys.size)    # size is 0 
puts hash.values         # nothing gets printed
puts hash.keys           #nothing gets printed.

So what is happening here? Where are the values getting stored, if they are not getting stored in the hash as keys or values.

in the next example, when Hash is defined as

hash = Hash.new {|hash, key| hash[key] = [] }
hash[:one] << "uno"
hash[:two] << "dos"
puts hash[:one]  #this is "uno"
puts hash[:two] #this is "dos"
puts hash[:three] # this is undefined.

I guess in the second example, hash is initializing all the keys with a blank array. So "uno" is getting appended to the empty array when "<<" this operator is used? I am confused about both the examples. I don't know what is happening in both of them. I couldn't find much information on this example in google as well. If somebody can help me explain these 2 examples it will be helpful. Thanks in advance

Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160
Sharvari Nagesh
  • 293
  • 3
  • 17

2 Answers2

5

hash = Hash.new([])

This statement creates an empty hash that has a default value of an empty array. If hash does not have a key k, hash[k] returns the default value, []. This is important: simply returning the default value does not modify the hash.

When you write:

hash[:one] << "uno" #=> ["uno"]

before hash has a key :one, hash[:one] is replaced by the default value, so we have:

[] << "uno" #=> ["uno"]

which explains why hash is not changed:

hash #=> {}

Now write:

hash[:two] << "dos" #=> ["uno", "dos"]
hash                #=> {}

To see why we get this result, let's re-write the above as follows:

hash = Hash.new([])       #=> {} 
hash.default              #=> [] 
hash.default.object_id    #=> 70209931469620 
a = (hash[:one] << "uno") #=> ["uno"] 
a.object_id               #=> 70209931469620 
hash.default              #=> ["uno"]  
b = (hash[:two] << "dos") #=> ["uno", "dos"] 
b.object_id               #=> 70209931469620 
hash.default              #=> ["uno", "dos"]  

So you see that the default array whose object_id is 70209931469620 is the single array to which "uno" and "dos" are appended.

If we had instead written:

hash[:one] = hash[:one] << "uno"
  #=> hash[:one] = [] << "uno" => ["uno"]
hash #=> { :one=>"uno" }

we get what we were hoping for, but not so fast:

hash[:two] = hash[:two] << "dos"
  #=> ["uno", "dos"] 
hash
  #=> {:one=>["uno", "dos"], :two=>["uno", "dos"]} 

which is still not what we want because both keys have values that are the same array.

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

This statements causes the block to be executed when hash does not have a key key. This does change the hash, by adding a key value pair1.

So now:

hash[:one] << "uno"

causes the block:

{ |h,k| h[k] = [] }

to make the assignment:

hash[:one] = []

after which:

hash[:one] << "uno"

appends "uno" to an empty array that is the value for the key :one, which we can verify:

hash #=> { :one=>"uno" }

This has the same effect as writing:

hash[:one] = (hash[:one] || []) << "uno"

(the expanded version of (hash[:one] ||= []) << "uno") when there is no default value.

Similarly,

hash[:two] << "dos" #=> ["dos"] 
hash #=> {:one=>["uno"], :two=>["dos"]}

which is usually the result we want.

1 The block need not change the hash, however. The block can contain any code, including, for example, { puts "Have a nice day" }.

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
4

hash = Hash.new(INITIAL_VALUE) is a syntax for creating hash, that has a default value. Default value is a “property” of the whole hash itself, it is to be returned when a non-existent key is accessed.

So, in your first example:

hash = Hash.new([]) # return a reference to an empty array for unknown keys

is the same as:

initial = []
hash = Hash.new(initial)

hence, when you call:

hash[:one] << "uno"

you in fact call hash[:one], that returns initial, and then call #<< method on it. In other words, these subsequent calls are the same as:

initial << "uno"
initial << "dos"

I guess, it is now clear, why all of them are sharing the same value. And the hash itself is still empty (on any call above, the initial is being used.) Look:

hash.merge! { one: "uno", three: "tres" }
hash[:one] # defined, since merge above
#⇒ "uno"
hash[:two] # undefined, initial will be returned
#⇒ []
hash[:two] << 'dos'
hash[:two] # defined, but defined as a reference to initial
#⇒ ["dos"]
hash[:two] = 'zwei' # redefined
#⇒ "zwei"
hash[:four] # undefined, initial
#⇒ ["dos"]

Hope it helps.

Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160