How do I use Array#dig and Hash#dig introduced in Ruby 2.3?
Solution 1
In our case, NoMethodError
s due to nil
references are by far the most common errors we see in our production environments.
The new Hash#dig
allows you to omit nil
checks when accessing nested elements. Since hashes are best used for when the structure of the data is unknown, or volatile, having official support for this makes a lot of sense.
Let's take your example. The following:
user.dig(:user, :address, :street1)
Is not equivalent to:
user[:user][:address][:street1]
In the case where user[:user]
or user[:user][:address]
is nil
, this will result in a runtime error.
Rather, it is equivalent to the following, which is the current idiom:
user[:user] && user[:user][:address] && user[:user][:address][:street1]
Note how it is trivial to pass a list of symbols that was created elsewhere into Hash#dig
, whereas it is not very straightforward to recreate the latter construct from such a list. Hash#dig
allows you to easily do dynamic access without having to worry about nil
references.
Clearly Hash#dig
is also a lot shorter.
One important point to take note of is that Hash#dig
itself returns nil
if any of the keys turn out to be, which can lead to the same class of errors one step down the line, so it can be a good idea to provide a sensible default. (This way of providing an object which always responds to the methods expected is called the Null Object Pattern.)
Again, in your example, an empty string or something like "N/A", depending on what makes sense:
user.dig(:user, :address, :street1) || ""
Solution 2
One way would be in conjunction with the splat operator reading from some unknown document model.
some_json = JSON.parse( '{"people": {"me": 6, ... } ...}' )
# => "{"people" => {"me" => 6, ... }, ... }
a_bunch_of_args = response.data[:query]
# => ["people", "me"]
some_json.dig(*a_bunch_of_args)
# => 6
Solution 3
It's useful for working your way through deeply nested Hashes/Arrays, which might be what you'd get back from an API call, for instance.
In theory it saves a ton of code that would otherwise check at each level whether another level exists, without which you risk constant errors. In practise you still may need a lot of this code as dig
will still create errors in some cases (e.g. if anything in the chain is a non-keyed object.)
It is for this reason that your question is actually really valid - dig
hasn't seen the usage we might expect. This is commented on here for instance: Why nobody speaks about dig.
To make dig
avoid these errors, try the KeyDial gem, which I wrote to wrap around dig
and force it to return nil/default if any error crops up.
user513951
Updated on July 09, 2022Comments
-
user513951 almost 2 years
Ruby 2.3 introduces a new method on
Array
andHash
calleddig
. The examples I've seen in blog posts about the new release are contrived and convoluted:# Hash#dig user = { user: { address: { street1: '123 Main street' } } } user.dig(:user, :address, :street1) # => '123 Main street' # Array#dig results = [[[1, 2, 3]]] results.dig(0, 0, 0) # => 1
I'm not using triple-nested flat arrays. What's a realistic example of how this would be useful?
UPDATE
It turns out these methods solve one of the most commonly-asked Ruby questions. The questions below have something like 20 duplicates, all of which are solved by using
dig
:How to avoid NoMethodError for missing elements in nested hashes, without repeated nil checks?
Ruby Style: How to check whether a nested hash element exists
-
Kadarach about 8 yearsI wish there was a
Hash#dig!
which would do the equivalent of multiple #fetch's. -
Drenmi about 8 years@Dogweather: You can try submitting a feature request in the issue tracker, and see if someone picks it up. :-)
-
matthew.tuck about 8 yearsAlternative idioms sometimes seen are
arr.fetch(:user, {}).fetch(:address, {})[:street1]
and for Rails,arr[:user].try(:[], :address).try(:[], :street1)
. -
Martin Verdejo almost 8 years@Drenmi are there possible instances where it is still preferable to use the old [:a][:b] syntax over dig, given I need to access a chain of keys down a hash?
-
Drenmi almost 8 years@MartinVerdejo: The only reason I can think of is if you're working on a code base that needs to maintain backwards compatibility with earlier rubies.
-
Tom about 6 years@matthew.tuck you need to be careful chaining
fetch
calls like that. If for examplearr = { user: nil }
will blow up withNoMethodError: undefined method fetch for nil:NilClass
-
jrochkind over 5 yearsYou have to be careful with
user.dig(:user, :address, :street1) || ""
, if the dig result was booleanfalse
, it'll be turned into empty string. -
Lori about 5 yearsIs there an
Array#something
wherea[b][c][d]
is equivalent toa.something([b, c, d])
? I could really use something like that right now.