Question

Conundrum:

something = {}
another = something
('a'...'f').each do |x|
  puts "another = #{another} and x= #{x} and something = #{something}"
  another = (another[x] ||= {})
end
puts something

Output:

=>another = {} and x= a and something = {}
=>another = {} and x= b and something = {"a"=>{}}
=>another = {} and x= c and something = {"a"=>{"b"=>{}}}
=>another = {} and x= d and something = {"a"=>{"b"=>{"c"=>{}}}}
=>another = {} and x= e and something = {"a"=>{"b"=>{"c"=>{"d"=>{}}}}}

Now at first glance it would seem like 'another' and 'something' are both pointing to the same hash object, but if that were case then in the output 'another' would match 'something'. So then how is the variable 'something' not an empty hash?

Was it helpful?

Solution

In Ruby, everything is an object and all objects are passed around using pointers (there are exceptions, but they don't apply here). When you do

something = {}
another = something

a single Hash object is created and both variables point to that same object. When you do

another[x] ||= {}

a single Hash object is created and a pointer to that object is added to the Hash pointed to by another. Another way to write that would be

old_hash = another
new_hash = {}
old_hash[x] = new_hash
another = new_hash

Remember: all of those assignments are just moving around pointers to objects. It should now be clear that another is pointing to the new (empty) hash. However, since old_hash also contains a pointer to the new hash, any futures changes to the new hash (i.e. another) will be seen by old_hash. That is why something grows with each iteration.

Your example is really no different from this simple situation:

x = {}
y = {}
z = {}
x["a"] = y
y["b"] = z
z["c"] = {}
puts x
# {"a"=>{"b"=>{"c"=>{}}}}

Your example just reverses the last three assignments - which makes no difference to the result.

OTHER TIPS

Now at first glance it would seem like 'another' and 'something' are both pointing to the same hash object, but if that were case then in the output 'another' would match 'something'.

Because something and another are not pointing to the same objects anymore. You reassigned another with another = (another[x] ||= {}). You can verify it by calling something.object_id and another.object_id.

Why another is always {}

Because when you do

another = (another[x] ||= {})

another is assigned value returned by evaluation of expression (another[x] ||= {}) which is {} and that's why another becomes {}

And, for your code to work i.e., the way you expected just remove the reassignment of another so another and something point to the same object.

something = {}
another = something
('a'...'f').each do |x|
  puts "another = #{another} and x= #{x} and something = #{something}"
  another[x] ||= {}  ## Change to this
end
puts something

OUTPUT:

another = {} and x= a and something = {}
another = {"a"=>{}} and x= b and something = {"a"=>{}}
another = {"a"=>{}, "b"=>{}} and x= c and something = {"a"=>{}, "b"=>{}}
another = {"a"=>{}, "b"=>{}, "c"=>{}} and x= d and something = {"a"=>{}, "b"=>{}, "c"=>{}}
another = {"a"=>{}, "b"=>{}, "c"=>{}, "d"=>{}} and x= e and something = {"a"=>{}, "b"=>{}, "c"=>{}, "d"=>{}}
 => "a"..."f" 
2.1.0 :265 >     puts something
{"a"=>{}, "b"=>{}, "c"=>{}, "d"=>{}, "e"=>{}}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top