Skip to content

Instantly share code, notes, and snippets.

@mischa
Created December 31, 2008 10:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mischa/41946 to your computer and use it in GitHub Desktop.
Save mischa/41946 to your computer and use it in GitHub Desktop.
#
# Why setting the default value of a Hash to be a Hash is wrong
#
# When I initialize a hash, setting the default value to be another hash,
# I am not able to set the value of the hash within the hash
#
# h = Hash.new({})
#
# h['bar']['baz] = 'foo' # Try to set the value of baz
#
# h['bar']['baz] #=> 'foo' # Check the value of baz. This appears to work.
#
# h #=> empty hash # ??? Despite having set baz and bar, h is still an empty hash.
#
# Let's see if we can figure out why this is happening
# and then work out a way to get what I want
# Here is the behavior that I want
def hash_test_normal
puts "-- Normal --"
r = Hash.new
r['foo'] = {}
hash_tests(r)
end
# Here is what I expected to give me that behavior
# However, instead Ruby keeps this single hash as the default value.
#
# Looking at Rubinious, it's clear that this is because Ruby is
# keeping the hash that I pass in as the default value in the
# instance variable @default.
#
# This means that it's not creating a new hash every time,
# but is instead just using the hash that I passed in.
#
# This makes sense, and I probably should have expected this behavior.
#
# Running this code, note that the default
# ends up being barbaz. Check rubinius/kernal/common/hash.rb if you don't
# understand why.
def hash_test_default
puts "-- Using default --"
r = Hash.new({})
hash_tests(r)
end
# So, the other option is to pass a block to Hash.new
# To see what the arguments to the block are, let's look at Rubinious:
#
# @default.call(self, key)
#
# So, self in this case will obviously be the hash.
#
# This suggests that I can mess with it to get the behavior that I want.
#
# Let's try...
def hash_test_explore
puts "-- Let's go exploring --"
r = Hash.new { |h, key| h[key] = {} }
hash_tests(r)
end
# It works!
#
# Note that I had for a second considered doing:
#
# h[key] ||= {}
#
# However, this does not work, because Ruby
# evaluates h[key], which, since it's Undefined,
# triggers the default_proc, which of course results
# in infinite recursion.
#
# Additionally, had I thought for one more second,
# I would have realized that there is no need
# for ||= because the only time the default_proc
# will get called is when the value for the hash key
# is Undefined.
# After this exercise, I feel stupid to have
# expected the original behavior. However, perhaps
# others will find it instructive.
def hash_tests(r)
puts "1 #{r['foo'].class} should equal Hash"
puts "2 #{r['foo']['bar'].class} should equal NilClass"
r['foo']['bar'] = 'baz'
puts "3 #{r['foo']['bar'].class} should equal String"
puts "4 #{r['foo']['bar']} should equal baz"
puts "5 #{r} should equal foobarbaz ****"
puts "6 #{r['foo'].class} should equal Hash"
puts "7 #{r['foo']['bar']} should equal baz"
puts "8 #{r['foo']} should equal barbaz"
puts "9 #{r.keys} should equal foo ***"
puts "10 Default: #{r.default}"
puts "10 Default proc: #{r.default_proc}"
end
hash_test_normal
puts "\n"
hash_test_default
puts "\n"
hash_test_explore
# So..
def hash_and_lambda
l = lambda{|n| n * n}
h = Hash.new{|s, k| k * k}
[:h, :l].each{|t| puts eval("#{t}[3]")}
end
hash_and_lambda
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment