Ruby Hash Lookup
Ruby has at least 3 different ways to get a value out of Hash
, and when to use each one depends largely how you want
to deal with a key that doesn’t exist.
Indexed
This is the way you’ll see most code:
# I'm using symbols for keys here, but of course this is an irrelevant detail.
h = Hash.new()
value = h[:nope] # => nil
A non-existent key will cause the Hash
lookup to simply return nil
.
Using fetch
Ruby also has a fetch()
method on Hash
.
h = Hash.new()
value = h.fetch(:nope) # => KeyError thrown.
Here a non-existent key will cause the Hash
lookup to throw a KeyError
exception.
Using fetch
with default
Fetch also has a second (or “1’th” for you 0-index types) optional parameter.
h = Hash.new()
value = h.fetch(:nope, 'hey there!') # => 'hey there!'
Using this version of fetch
will provide a default value IF the key doesn’t exist. Do note that this is does not
mutate the original hash in any way; the key STILL doesn’t exist if it didn’t before.
h = Hash.new()
value = h.fetch(:nope, 'hey there!') # => 'hey there!'
h # => {} :nope still doesn't exist
If you want the behavior of mutating the array on a non-existent key access, you can use the default value constructor. This will provide a default value for any key access for which the key doesn’t exist, but the default value is the SAME FOR ALL KEYS; the “fetch with default” scheme can be used to have a DIFFERENT default per key lookup. More info here.
When To Use What?
Like in so much of development, “it depends”. I will posit that the fetch
method should be used mostly only when you
REALLY, REALLY expect the key to be there, and it’s A Bad Thing(tm) if it isn’t. Mainly because exceptions should
be… exceptional; reserved for if they happen, you have a bug, or someone else has a bug, or there’s a bug. Something
that needs to be looked at. This is a bit of a hot take and different teams view this differently, and that’s ok. But
in general, I feel that exception based flow-logic is a bit harder to understand and should be avoided where possible.
The indexed
method is probably the most common and idiomatic. It does return nil
which generally needs to be
checked specifically since nil
is almost never the type or value that you want, but it’s also “falsey”, so can be put
inline into a conditional.
h = {}
if h[:nope]
puts "Wait, this shouldn't happen"
else
rule_the_world()
end
I really like fetch with default
, but it’s for use cases where the non-existent key issue is expected, and you can
actually provide a default at lookup time.
I use it a lot in the “change multiple if’s to a lookup table” refactoring pattern.
# Example in mechanics, not quality.
value = if some_hash[:foo] == 0
"zero"
elsif some_hash[:foo] == 1
"one"
else
"many"
end
can be refactored to:
lookup_table = { 0 => "zero", 1 => "one" }
value = lookup_table.fetch(:foo, "many") # yes, sometimes `case` can be better here.