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 Hash
es.
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 nil
s”.
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 nil
s 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