In Progress
Unit 1, Lesson 1
In Progress

Hash Default Blocks

Video transcript & code

In Episode 12, we looked at how to use the #fetch method to provide a default value when retrieving values from a Hash. In some cases, we'd like to have the same default behavior no matter where the hash is used. When this is the case, we can use a default block, provided when the hash is instantiated.

For instance, consider a hash that is used to count the number of times words are used in text. Whenever a word is first found, the corresponding hash value has to be set to 0 so that it can be incremented successfully.

word_count = {}
text = <<END
I'm your only friend
I'm not your only friend
But I'm a little glowing friend
But really I'm not actually your friend
But I am
END

text.split.map(&:downcase).each do |word|
  word_count[word] ||= 0
  word_count[word] += 1
end
word_count
# => {"i'm"=>4,
#     "your"=>3,
#     "only"=>2,
#     "friend"=>4,
#     "not"=>2,
#     "but"=>3,
#     "a"=>1,
#     "little"=>1,
#     "glowing"=>1,
#     "really"=>1,
#     "actually"=>1,
#     "i"=>1,
#     "am"=>1}

Instead of defaulting the count to 0 in the word-count code, we can instantiate the hash with a default block. This block will be called if, and only if, an attempt is made to retrieve a missing value. The block will receive the hash itself, and the missing key, as arguments. Inside, we set the value to 0. The result of the assignment is the new value, so we are also implicitly returning 0 from it.

word_count = Hash.new do |hash, missing_key|
  hash[missing_key] = 0
end
text = <<END
I'm your only friend
I'm not your only friend
But I'm a little glowing friend
But really I'm not actually your friend
But I am
END

text.split.map(&:downcase).each do |word|
  word_count[word] += 1
end
word_count
# => {"i'm"=>4,
#     "your"=>3,
#     "only"=>2,
#     "friend"=>4,
#     "not"=>2,
#     "but"=>3,
#     "a"=>1,
#     "little"=>1,
#     "glowing"=>1,
#     "really"=>1,
#     "actually"=>1,
#     "i"=>1,
#     "am"=>1}

We can access the default block after the hash is initialized by sending it the #default_proc message.

h = Hash.new do |hash, missing_key|
  hash[missing_key] = 0
end
h.default_proc # => #<Proc:0x87e23b0@-:1>
h.default_proc.call({}, :foo) # => 0

We can take advantage of this fact in with a particularly elegant idiom in which we create a hash whose default value is another hash, whose default value is another hash… and so on. This enables us to set hash keys nested to an arbitrary depth, without first initializing the intervening hashes. This is can be handy for configuration variables.

config = Hash.new do |h,k|
  h[k] = Hash.new(&h.default_proc)
end

config[:production][:database][:adapter] = 'mysql'
config[:production][:database][:adapter] # => "mysql"

One way I like to use this capability of hashes is to cache expensive operations. For instance, here's a hash whose default behavior is to fetch a weather report. The hash key is the name of geographical region. If the key hasn't been referenced before, the weather temperature for that region is fetched. If the same region is retrieved again, the saved value in the hash will be returned instead of another HTTP request being made.

I'd demonstrate this code for you live, but Google is giving me 403s at the moment.

require 'open-uri'
require 'cgi'
require 'nokogiri'
temperature = Hash.new do |h, town|
  url = "http://www.google.com/ig/api?weather=" +
    CGI.escape(town)
  h[town] = open(url) do |body|
    Nokogiri::XML(body).
      at_css('current_conditions temp_f')['data']
  end
end

temperature['Shrewsbury, PA']       # => "64"

And there you have it: how to create a hash with a default block. Until next time, happy hacking!

Responses