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('')
end
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
end
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.
Comments
There are no existing comments
Got your own view or feedback? Share it with us below …