Ruby offers the fetch method to return values from a Hash object based on a key argument. This method can take a second argument, which will be returned when the key doesn't exist within the Hash. But if you are not careful you may be executing code unintentionally which is not great if that code has side-effects, or is costly to run.

A hash is a data structure that stores values against an associated key, you then use the same key when you want to retrieve, or update the associated value. The Ruby standard library defines a Hash class which allows us to store and manipulate elements in a hash as follows:

h = { "a" => "AAA", "b" =>"BBB", "c" => nil }
puts h["a"]    # Outputs 'AAA'
puts h["b" ]   # Outputs 'BBB'
puts h["c"]    # Outputs 'nil'
h["b"] = "bbb"
puts h    # Outputs '{"a"=>"AAA", "b"=>"bbb", "c"=>nil}
puts h["z"]    # Outputs 'nil'

As you can see, if we try to access a key that does not exist (h["z"]) we get a nil value returned. But this is indistinguishable from the case where the key is defined to have a nil value, e.g. h["c"]. To help distinguish these cases we can use the fetch method on Hash, which takes the key that you wish to retrieve. If the key doesn't exist fetch will raise a KeyError.

h = { "a" => "AAA", "b" =>"BBB", "c" => nil }
h.fetch("c")    # Returns 'nil'
h.fetch("z")    # Raises a KeyError

So we see that the fetch method provides this convenient distinction between non-existent keys and existing nil-valued keys.

We can also use fetch to handle default values, in the event that the key is not present. A common pattern for returning a default value is using the logical OR operator (||), as follows:

h = { "a" => "AAA", "b" =>"BBB", "c" => nil }
h["a"] ||  "Not here"    # Returns 'AAA'
h["c"] ||  "Not here"    # Returns 'Not here'
h["z"] ||  "Not here"    # Returns 'Not here'

The fetch method provides the capability of returning a default value in the case where the key is not found. This default is provided as the second argument to the method call, for example:

h = { "a" => "AAA", "b" =>"BBB", "c" => nil }
h.fetch("a", "Not here")    # Returns 'AAA'
h.fetch("c", "Not here")    # Returns 'nil'
h.fetch("z", "Not here")    #  Returns 'Not here' (no KeyError raised)

As before, this usage of fetch allows us to distinguish between the non-existent keys (e.g. h["z"]) and the nil-valued keys (e.g. h["c"]).

Often there may some non-trivial logic required to determine the default value to be used in each case, so it can be convenient to use a method which returns our default value, for instance:

def default_value
  ["N", "o", "t", " ", "h", "e", "r", "e"].join('')

h = { "a" => "AAA", "b" =>"BBB", "c" => nil }

h.fetch("a", default_value)    # Returns 'AAA'
h.fetch("c", default_value)    # Returns 'nil'
h.fetch("z", default_value)    # Returns 'Not here'

But we must be wary here!

Whilst they return the same values, the previous formulation is not equivalent to this

h["a"] || default_value    # Returns 'AAA'
h["z"] || default_value    #  Returns 'Not here'

In the former method, using fetch, the default_value method is called even if we never need the default value. This is because we are passing the default_value as a method argument, so it must be evaluated before invoking the fetch method. In the simple example above this has little impact, but this becomes extremely important if the default_value method is expensive to compute, or if it has a side-effect. Consider, for example:

def default_value
  puts "SIDE EFFECT: We only want to see when Hash key is missing"
  "Not here" # returning same default value

h = { "a" => "AAA", "b" =>"BBB", "c" => nil }

h.fetch("a", default_value)    # Returns 'AAA' and message printed
h.fetch("c", default_value)    # Returns 'nil' and message printed
h.fetch("z", default_value)    #  Returns 'Not here' and message printed

h["a"] || default_value    # Returns 'AAA' and no message

If we run this example we see the expected return values in each case, but we see that, with fetch, the message (or side-effect) is also printed on each invocation. But we only want this side-effect to fire when the key is missing!

By contrast, using the || pattern will avoid calling default_value when the key is found, which is the behaviour we want.

So how do we get the benefits of both? We want the nil-handling of fetch, whilst only calling the default_value when the key is not found.

To achieve this we can pass a block to the fetch method. The block will only be called in the event that the key cannot be found and the value returned by the block will be used as the default value:

h.fetch("a"){ default_value }    # Returns 'AAA' and no message printed
h.fetch("c"){ default_value }    # Returns 'nil' and no message printed
h.fetch("z"){ default_value }   #  Returns 'Not here' and message printed

Equivalently we can convert our default_value function to a Proc using the & syntax:

h.fetch("a", &method(:default_value))    # Returns 'AAA' and no message printed
h.fetch("c", &method(:default_value))    # Returns 'nil' and no message printed
h.fetch("z", &method(:default_value))    #  Returns 'Not here' and message printed

In both of these solutions the block (or Proc) will only be invoked if the key cannot be found, we correctly handle nil-valued keys and we avoid unintented invocations of our default method.

Update: As pointed out by a commenter on Reddit, this second method will allocate a costly Method object on each call, so you better sticking with the block version.


There are no existing comments

Got your own view or feedback? Share it with us below …



Join our mailing list to hear when new content is published to the VectorLogic blog.
We promise not to spam you, and you can unsubscribe at any time.