Daily Code Reading #1 – Facets Hash#autonew

I’m starting this series by taking a look at Ruby Facets. Facets is a collection of Ruby core and Ruby standard library extensions. It has a lot of good code embedded in it from many contributors so it should be a good place to start looking for some new ideas.

The Code

1
2
3
4
5
6
# File lib/core/facets/hash/autonew.rb, line 19
  def self.autonew(*args)
    #new(*args){|a,k| a[k] = self.class::new(*args)}
    leet = lambda { |hsh, key| hsh[key] = new( &leet ) }
    new(*args,&leet)
  end

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/usr/bin/ruby -wKU
#
# Code Reading #1
require '../base'
require 'facets/hash/autonew'
 
class HashAutonew
  def data
    @data = Hash.autonew
    @data['users']['eric']['blog'] = 'http://theadmin.org'
    @data
  end
end
 
ap(HashAutonew.new.data)
 
class HashAutonewTest < Test::Unit::TestCase
  def test_should_nest_values_automatically
    hashish = HashAutonew.new.data
 
    assert hashish.key?('users')
    assert hashish['users'].key?('eric')
    assert hashish['users']['eric'].key?('blog')
    assert_equal 'http://theadmin.org', hashish['users']['eric']['blog']
  end
 
end

On github

Review

Hash#autonew is wrapping Hash#new using #new‘s block form. I can’t dig to deep into Hash#new because it’s implemented in C but it looks like the lambda is using recursion to set a bunch of default values for the hash when it’s accessed with child elements. Hash#new supports this but it only works one level deep:

1
2
3
4
5
6
7
8
9
10
11
>> Hash.new
{}
>> Hash.new{|hash, key| 'default'}
{}
>> h = Hash.new{|hash, key| 'default'}
{}
>> h['s']
"default"
>> h['s']['s']['s']
NoMethodError: undefined method `[]' for nil:NilClass
        from (irb):5

So in my test:

  1. @data is getting a ‘users’ key with the value of
  2. a new hash with the key ‘eric’ with the value of
  3. a new hash with the value of ‘http://theadmin.org’
  4. (recursion stops)

The recursion stops with the ‘http://theadmin.org’ string because #autonew is passing String#new the leet block but String#new doesn’t do anything with the block and just returns the string.

Sidebar: Precise loading

I noticed that facets provides many different ways to load it’s extensions:

  • require 'facets' – load everything
  • require 'facets/hash' – only load the Hash extensions
  • require 'facets/hash/autonew' – only load the Hash#autonew extension

This is very useful since it lets a developer only extend what they need and is a nice change from ActiveSupport which loads everything at once.