Created
December 31, 2008 10:58
-
-
Save mischa/41946 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# | |
# 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