Ruby Tuesday – Hash#fetch

Today’s Ruby Tuesday is on Hash#fetch.

Hash#fetch is one of those methods that would have to go in my Top 10 of most underutilized methods in Ruby.

Why would Hash#fetch make that list? Because it makes explicit the results of a key lookup in a Hash.

If we have a Hash constructed using a hash literal, and do a key lookup using the square brackets, it will return a nil when a key is not found, as nil is the default default-value for Hashes.

hash1 = {:a => 1, :b => 2, :c => 3}
# => {:a=>1, :b=>2, :c=>3}
hash1[:a]
# => 1
hash1[:d]
# => nil

You might be thinking, “That’s not so bad, I can just check for nils”.

What happens when there is a key that references nil? Now you can’t know if nil was in there because it was “valid”, or just returned.

hash2 = {:a => 1, :b => 2, :c => 3, :d => nil}
# => {:a=>1, :b=>2, :c=>3, :d=>nil}
hash2[:d]
# => nil

Or what happens when you get a Hash that was not created with a Hash literal?

hash3 = Hash.new {|hash, key| "foo"}
# => {}
hash4 = Hash.new("foo")
# => {}
hash3[:a]
# => "foo"
hash4[:a]
# => "foo"

Oops. We no longer get a nil back that we can check against.

This is where Hash#fetch becomes nice and explicit.

If we call Hash#fetch and no key is found, it will raise an key not found exception.

hash2.fetch(:d)
# => nil
hash1.fetch(:d)
# KeyError: key not found: :d
# from (pry):8:in `fetch'

Why is this nice? Well, it means we encountered and error, and we stop execution at the point of the error, instead of propagating nils throughout the code base to crash at some later point.

If we don’t want an exception, we can also pass in a default value to Hash#fetch allowing us to control the default value when a key is not found.

hash1.fetch(:d, :key_not_found)
# => :key_not_found
hash2.fetch(:d, :key_not_found)
# => nil
hash3.fetch(:a, :key_not_found)
# => :key_not_found
hash4.fetch(:a, :key_not_found)
# => :key_not_found

It even works when the Hash fetch is being called on specifies different default behavior.

Even when that default behavior would try to add the key into the Hash with a default value.

hash5 = Hash.new {|hash, key| hash[key] = "foo"}
# => {}
hash5.fetch(:d)
# KeyError: key not found: :d
# from (pry):43:in `fetch'
hash5.fetch(:d, "bar")
# => "bar"

Hash#fetch can also take a block that will be called with the key as an argument, when the key is not found in the Hash.

hash1.fetch(:d) do |key|
  puts "no key found for key: `#{key}`"
  :no_key_found
end
# no key found for key: `d`
# => :no_key_found

I encourage you to play with it, and see if it doesn’t start to creep up into your list of favorite methods that are underutilized.

–Proctor