Sonntag, 16. August 2015

Dive into the Hash#default_proc!

Hash without Proc

A Ruby Hash can be initialized in several ways:
config = {}
config = Hash.new
In both cases the associative array returns nil, when accessed with a key which does not exist:
config[:production]
=> nil
The Hash was instantiated without a Proc:
config.default_proc
=> nil
If you want to execute a nested access, an exception is raised:
config = Hash.new
config[:production][:log] = 'log.txt'
NoMethodError: undefined method '[]=' for nil:NilClass
because Nil#[] is not defined.

Hash with Proc

A Hash also can be instantiated with a block:
config = Hash.new{ |hash, key| rand(1..10) }
The access to an unknown key will always return a value (random number between 1 and 10):
config[:production]
=> 4
config[:production]
=> 9
The reason is the Hash object got a default Proc during instantiation. If the accessing key is not included, the Proc is executed. In that example the Hash itself keeps being empty:
config
=> {}
Of course, the Proc also can do more meaningful stuff:
config = Hash.new{ |hash, key| hash[key] = Hash.new }
config[:production][:log] = 'log/production.log'
Although the key :production has never been initialized, no exception is thrown. The Hash was even extended as expected:
config
=> {:production=>{:log=>"log/production.log"}}
The Proc object can also be requested again:
config.default_proc
=> #
However, the Hash still can not be accessed infinitely deep:
config[:production][:log][:path] = 'log/production.log'
NoMethodError: undefined method '[]=' for nil:NilClass

Hash with Proc for infinite access

When a Hash has to provide infinite deep access without raising an exception, a default Proc has to be passed recursively by passing the default_proc to the accessed Hash value:
config = Hash.new do |hash, key| 
  hash[key] = Hash.new(&hash.default_proc)
end
The Hash owns a default Proc:
config.default_proc
=> #
If the accessing key is not available, no matter which depth, it will always create a new Hash object by default:
config[:production][:log][:path] = 'log/production.log'
config
=> {:production=>{:log=>{:path=>"log/production.log"}}}
A default Proc is also assignable after instantiating the Hash:
config = {}
config.default_proc = proc{ |hash, key| hash[key] = Hash.new(&hash.default_proc) }
config[:production][:log][:path] = 'log/production.log'
config
=> {:production=>{:log=>{:path=>"log/production.log"}}}
Not infinite depth, but very deep:
config[0][1][2][3][4][5][6][7][8][9]
=> {}

Further articles of interest:

Supported by Ruby 2.2.1