How does shovel (<<) operator work in Ruby Hashes?

RubyHash

Ruby Problem Overview


I was going through Ruby Koans tutorial series, when I came upon this in about_hashes.rb:

def test_default_value_is_the_same_object
  hash = Hash.new([])

  hash[:one] << "uno"
  hash[:two] << "dos"

  assert_equal ["uno", "dos"], hash[:one]
  assert_equal ["uno", "dos"], hash[:two]
  assert_equal ["uno", "dos"], hash[:three]

  assert_equal true, hash[:one].object_id == hash[:two].object_id
end

The values in assert_equals, is actually what the tutorial expected. But I couldn't understand how there is a difference between using << operator and = operator?

My expectation was that:

  • hash[:one] would be ["uno"]
  • hash[:two] would be ["dos"]
  • hash[:three] would be []

Can someone please explain why my expectation was wrong?

Ruby Solutions


Solution 1 - Ruby

You have mixed up the way this works a bit. First off, a Hash doesn't have a << method, that method in your example exists on the array.

The reason your code is not erroring is because you are passing a default value to your hash via the constructor. http://ruby-doc.org/core-1.9.3/Hash.html#method-c-new

hash = Hash.new([])

This means that if a key does not exist, then it will return an array. If you run the following code:

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

Then you will get an undefined method error.

So in your example, what is actually happening is:

hash = Hash.new([])

hash[:one] << "uno"   #hash[:one] does not exist so return an array and push "uno"
hash[:two] << "dos"   #hash[:two] does not exist, so return the array ["uno"] and push "dos"

The reason it does not return an array with one element each time as you may expect, is because it stores a reference to the value that you pass through to the constructor. Meaning that each time an element is pushed, it modifies the initial array.

Solution 2 - Ruby

When you're doing hash = Hash.new([]) you are creating a Hash whose default value is the exact same Array instance for all keys. So whenever you are accessing a key that doesn't exist, you get back the very same Array.

h = Hash.new([])
h[:foo].object_id # => 12215540
h[:bar].object_id # => 12215540

If you want one array per key, you have to use the block syntax of Hash.new:

h = Hash.new { |h, k| h[k] = [] }
h[:foo].object_id # => 7791280
h[:bar].object_id # => 7790760

Edit: Also see what Gazler has to say with regard to the #<< method and on what object you are actually calling it.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionbitsView Question on Stackoverflow
Solution 1 - RubyGazlerView Answer on Stackoverflow
Solution 2 - RubyDominik HonnefView Answer on Stackoverflow