Access Ruby Hash Values with Fallbacks for Missing Data

Rob Whittaker

Some of you may not been aware but there is more than one way to get a value out of a hash. By the end of this article I will have told you about three of them and the relative merits of each method.

The first is the one most Ruby developers know and love, #[]. This is the method we see when we first learn about hashes. It is how programmers from other languages might access elements in arrays or hashes and we use it as follows.

pets = { cat: "Jess" }
# => "Jess"

This is great. It does exactly what we want it to do. What if we have a situation where the value might not exist? What happens then? Lets have a look.

pets = { cat: "Jess" }
# => nil

It’s all right, you think. I’ll deal with the nil.

pets = { cat: "Jess" }
pets[:dinosaur] || "They all died :("
# => "They all died :("

What if, for some crazy reason, your key is not nil, but false? You could then end up fallbacking to a value you never intended to.

dinosaurs = { alive?: false }
dinosaurs[:alive?] || true
# => true

What if the key does not exist in the hash at all? You would never know this, unless you put some guard clause or error handling in place.

There is an answer to both of these scenarios and its name is #fetch.

#fetch works in much the same way as #[], but it also allows you to set a fallback value for when the key does not exist or it is nil.

pets = { cat: "Jess" }
pets.fetch(:dinosaur, "They all died :(")
# => "They all died :("

You may been wondering how this is any better than our example from earlier. Let’s look at another example.

dinosaurs = { alive?: false }
dinosaurs.fetch(:alive?, true)
# => false

This seems to give a much better answer than our original solution. #fetch only falls back to the default if the key does not exist or if the key’s value is nil. Here the :alive? is false. This is the returned value.

Now, what if we wanted to do something crazy like this.

pets.fetch(:dinosaur, Dinosaur.raise_from_the_dead!)

When we use #fetch with two arguments, the second argument is always evaluated even if it is never used. This could be an expensive operation. Think of all the time and effort this would take. You would have to spend time finding some amber with a mosquito in it. Then there is all the research and development behind the science. In the end we all know life finds a way.

What we actually want is a way to only use the fallback if we need it. Luckily such an operation exists.

pets.fetch(:dinosaur) { Dinosaur.raise_from_the_dead! }

Now we will only be raising dinosaurs from the dead when we do not already have a pet dinosaur. This seems sensible. Who has space for two dinosaurs? This is particularly helpful when trying to introduce dependency injection.

You should now understand three ways to read a value from a hash, their relative merits, and when to use them. Good luck!