In Perl, you can create a hash of arbitrary depth like so:
$hash->{foo}->{bar}->{baz} = 123;
I've tried this in ruby:
hash[:foo][:bar][:baz] = 123
but I get nil.= errors
I could of course do a check for null at each level, but oy, that's so
cumbersome. Is there a ruby way?
Yes.
You need to understand Ruby a little better, though.
h = {}
p h[:a]
=> nil
That is, if you have a hash, and you reference an element in the hash
that does not exist, the default behavior is for the hash to return
nil.
So, if you do this:
h = {}
p h[:a][:b]
You will get this:
NoMethodError: undefined method `[]' for nil:NilClass
This is because h[:a] returns a nil, as you already discovered, and
NilClass does not define an [] method. Since, in Ruby, you are just
chaining method calls, your h[:a][:b][:c] style syntax can only work
if the return element from any given level is an object that provides
an [] method.
Now, consider this example:
h = Hash.new {|h,k| h[k] = {}}
p h[:a]
=> {}
Ah! Now we're on to something.
h = Hash.new {|h,k| h[k] = {}}
p h[:a][:b]
=> nil
Hmmm.
h = Hash.new {|h,k| h[k] = {}}
p h[:a][:b][:c]
=> NoMethodError: undefined method `[]' for nil:NilClass
Oh, dang. Do you see what's happening here? Hash's new method can
take a block. If it is given a block, then it will call that block
with arguments of the hash, itself, and the key that was given, if
it's asked to lookup a key that does not exist in the hash. So, h[:a]
ran that block, creating a value at :a => {}. h[:a][:b], though, gets
us back to the default Hash behavior. Still, you see one solution
here, I bet:
h = Hash.new {|h,k| h[k] = Hash.new {|h2,k2| h2[k2] = {}}}
h[:a][:b][:c] = 123
p h[:a][:b][:c]
=> 123
That works. It's kind of hard to easily read, though. So, you could
rewrite it like this:
h = Hash.new do |h1,k1|
h1[k1] = Hash.new do |h2,k2|
h2[k2] = {}
end
end
That, at least, lets the eye follow along a little better, but it's
still limited because you must decide what depth of auto
initialization you are going to support ahead of time. If you try
something like h[:a][:b][:c][:d][:e] you will again get that
unpleasant NoMethodError.
However, step back just a moment. The hash initializer accepts a
block. And, if you look at the Hash methods, you'll see a
Hash#default_proc.
It does just what it sounds like it does -- it returns the default
proc, which is what the block is turned into when the new method
captures it and stores it.
So, all you need to do is this:
h = Hash.new {|hash,key| hash[key] = Hash.new(&hash.default_proc)}
h[:a][:b][:c][:d][:e] = 12345
p h[:a][:b][:c][:d][:e]
=> 12345
Hopefully this helps you follow what's happening with Ruby's method
calls and Hash initializers.
Kirk Haines