14

I have the following hash:

hash = {'name' => { 'Mike' => { 'age' => 10, 'gender' => 'm' } } }

I can access the age by:

hash['name']['Mike']['age']

What if I used Hash#fetch method? How can I retrieve a key from a nested hash?

As Sergio mentioned, the way to do it (without creating something for myself) would be by a chain of fetch methods:

hash.fetch('name').fetch('Mike').fetch('age')
ndnenkov
  • 35,425
  • 9
  • 72
  • 104
PericlesTheo
  • 2,429
  • 2
  • 19
  • 31

8 Answers8

48

From Ruby 2.3.0 onward, you can use Hash#dig:

hash.dig('name', 'Mike', 'age')

It also comes with the added bonus that if some of the values along the way turned up to be nil, you will get nil instead of exception.

You can use the ruby_dig gem until you migrate.

ndnenkov
  • 35,425
  • 9
  • 72
  • 104
  • 21
    That is _really_ not a bonus. – Duncan Bayne Sep 06 '16 at 01:31
  • 1
    @DuncanBayne in dynamically typed languages where using `nil` as falsy value is an idiom - it is. Consider the older rails alternative, which is to chain `try` invocations - it would do the same. – ndnenkov Sep 06 '16 at 06:13
  • 1
    I believe the correct alternative is to chain `fetch` invocations, which will raise an error by default if a key is not found. Then one can carefully, consciously provide default values as needed. I've seen many bugs caused by code - both Ruby and Coffeescript - blithely treating missing values as nil. – Duncan Bayne Sep 06 '16 at 12:17
  • 1
    @DuncanBayne, with that reasoning you would advise people to surround half of their methods with `rescue`/`if` blocks for state checking. Probably everyone should skip normal indexing and go with `#fetch` always... you simply don't know why you got that `nil` otherwise? The question is - do you intentionally care. Even in java/c# it should be the other way around - *if you are **aware** that things might be missing from one nesting level onwards* and *you **know** that is a problem* then *you should be trying to handle those cases*. Or better - make your structure consistent (but that varies). – ndnenkov Sep 06 '16 at 14:40
  • 2
    That's it in a nutshell: "The question is - do you intentionally care." I prefer to use `fetch` to a) encourage myself to think carefully about whether I care, and _especially_ b) make it apparent to those reading the code whether I care or not. – Duncan Bayne Sep 06 '16 at 21:50
  • @DuncanBayne, (a) you can do that regardless (b) you don't simply make it apparent - you literally say *I always care*. Which shouldn't be the case 99% of the time. Unless you are launching shuttles, you shouldn't check if something unexpected happened just so that you can fail more explicitly. There is a good chance the language designers are well aware of the everyday use cases. But I would agree that it would be nice if they allow you to give a block that is used for default value. You can raise an error there if you so desire. – ndnenkov Sep 07 '16 at 06:59
  • 1
    I think there's a use-case for both `[]` and for `fetch`. Sometimes failure should raise an error, and sometime a `nil`. The problem I think @DuncanBayne is conveying is that there's already a clean way to chain the former (`[:foo][:bar]`), but no nice way to chain the latter (`.fetch(:foo).fetch(:bar)` is needlessly verbose). It'd be nice if `dig` provided this latter; instead, it handles the former case, which seems pretty redundant. – Simon Lepkin Nov 18 '16 at 18:22
  • @SimonLepkin, that is precisely the problem - there is no nice way to do it without raising an exception other than `dig`. If you do `x[:foo][:bar]` then you will get error trying to index `nil` if `x` didn't have `:foo`. – ndnenkov Nov 19 '16 at 08:35
  • @ndn Oh, yep, you're right, I take back that bit about redundancy. – Simon Lepkin Nov 21 '16 at 17:34
  • 2
    This is an incorrect answer. fetch raises an error if a key is not there. dig does not. It's fundamentally different. – dgmora Aug 14 '18 at 12:49
  • @dgmora in most of the cases writing Ruby, you would actually want that to be the case. Still, if it doesn't fit your needs, you can use one of the other answers. I think the spirit of the question was *"I have a list of keys, how do I access the nested thingy using these keys"* rather than being focused on the error raising per se. – ndnenkov Aug 14 '18 at 12:56
  • 1
    But that's not the question. He knows how to access the nested thingy, he's asking how to do it with fetch. The answer is also not accepted: The accepted one is the nested fetch. It's a useful answer but the question is another. – dgmora Aug 14 '18 at 13:04
  • 1
    @dgmora I don't think that is the case. OP knew how to access nested thingies given a static list of keys. He wanted to know what to do with a dynamic list. As per OP's own comments, [he is searching for something more elegant than chaining `fetch` invocations](https://stackoverflow.com/questions/19115838/how-do-i-use-the-fetch-method-for-nested-hash/34820070?noredirect=1#comment28265474_19115838) and [he wants something built into the language](https://stackoverflow.com/questions/19115838/how-do-i-use-the-fetch-method-for-nested-hash/34820070?noredirect=1#comment28265531_19115838). – ndnenkov Aug 14 '18 at 13:58
  • As for why this answer isn't the accepted one - I'm not saying Sergio's answer is any less valid. But still, the fact that the question was posted 2013 and my answer was posted in 2016 plays a role. When I wrote this, Sergio's answer has been the accepted one for a while. – ndnenkov Aug 14 '18 at 14:00
  • Discussing for discussing is pointless. You win, I don't care much. This answer does something different than fetch(a).fetch(b) And the question is how to use fetch for a nested hash. This answer also doesn't do that, it does something similar. – dgmora Aug 15 '18 at 14:22
  • @dgmora, I'm not discussing for the sake of discussing. I'm discussing because you brought it up and I disagree with your point. As I said, I don't think OP was that focused on `fetch` per se, more so on access with dynamic key list. Look at the accepted answer as well. It doesn't use `fetch` either. It just mentions that you can use `fetch` instead of indexing and it will still work. None of the other answers focuses on `fetch`. They are all about the dynamic list of keys. – ndnenkov Aug 15 '18 at 14:39
7

EDIT: there is a built-in way now, see this answer.


There is no built-in method that I know of. I have this in my current project

class Hash
  def fetch_path(*parts)
    parts.reduce(self) do |memo, key|
      memo[key.to_s] if memo
    end
  end
end

# usage
hash.fetch_path('name', 'Mike', 'age')

You can easily modify it to use #fetch instead of #[] (if you so wish).

Sergio Tulentsev
  • 226,338
  • 43
  • 373
  • 367
1

As of Ruby 2.3.0:

You can also use &. called the "safe navigation operator" as: hash&.[]('name')&.[]('Mike')&.[]('age'). This one is perfectly safe.

Using dig is not safe as hash.dig(:name, :Mike, :age) will fail if hash is nil.

However you may combine the two as: hash&.dig(:name, :Mike, :age).

So either of the following is safe to use:

hash&.[]('name')&.[]('Mike')&.[]('age')
hash&.dig(:name, :Mike, :age)
Sergio Tulentsev
  • 226,338
  • 43
  • 373
  • 367
thisismydesign
  • 21,553
  • 9
  • 123
  • 126
  • The whole point of `fetch` is that you _want_ an error raised instead of a silent failure, and `dig` and `[]` don't offer that regardless of `&.`. Giving nil instead of an error is only safe if your code plans to do something with the nil later on, otherwise it can create an extremely subtle silent bug that you'd rather know about with a clear error at the point of contract violation. Similarly, if the contract is that the hash is defined, it's better to use a plain `.` and raise if it's not defined so you can fix the bug, but that's not what OP's asking about. – ggorlen Jun 24 '21 at 01:35
  • Good point about `fetch`. I think not raising an error is considered an upside about `dig` but it depends on your expectations. – thisismydesign Jun 24 '21 at 11:31
1

If you don't want to monkey patch the standard Ruby class Hash use .fetch(x, {}) variant. So for the example above will look like that:

hash.fetch('name', {}).fetch('Mike', {}).fetch('age')
ka8725
  • 2,788
  • 1
  • 24
  • 39
0

If your goal is to raise a KeyError when any of the intermediate keys are missing, then you need to write your own method. If instead you're using fetch to provide default values for missing keys, then you can circumvent the use of fetch by constructing the Hashes with a default values.

hash = Hash.new { |h1, k1| h1[k1] = Hash.new { |h2, k2| h2[k2] = Hash.new { |h3, k3| } } }
hash['name']['Mike']
# {}
hash['name']['Steve']['age'] = 20
hash
# {"name"=>{"Mike"=>{}, "Steve"=>{"age"=>20}}}

This won't work for arbitrarily nested Hashes, you need to choose the maximum depth when you construct them.

Max
  • 21,123
  • 5
  • 49
  • 71
0

A version that uses a method instead of adding to the Hash class for others using Ruby 2.2 or lower.

def dig(dict, *args)
  key = args.shift
  if args.empty?
    return dict[key]
  else
    dig(dict[key], *args)
  end
end

And so you can do:

data = { a: 1, b: {c: 2}}
dig(data, :a) == 1
dig(data, :b, :c) == 2
quaspas
  • 1,351
  • 9
  • 17
0

The point of fetch is that an explicit error is raised at the point of contract violation instead of having to track down a silent nil running amok in the code that can lead to unpredictable state.

Although dig is elegant and useful when you expect nil to be a default, it doesn't offer the same error reporting guarantees of fetch. OP seems to want the explicit errors of fetch but without the ugly verbosity and chaining.

An example use case is receiving a plain nested hash from YAML.load_file() and requiring explicit errors for missing keys.

One option is to alias [] to fetch as shown here, but this isn't a deep operation on a nested structure.

I ultimately used a recursive function and hash.instance_eval {alias [] fetch} to apply the alias to such a plain hash deeply. A class would work just as well, with the benefit of a distinct subclass separate from Hash.

irb(main):001:1* def deeply_alias_fetch!(x)
irb(main):002:2*   if x.instance_of? Hash
irb(main):003:2*     x.instance_eval {alias [] fetch}
irb(main):004:2*     x.each_value {|v| deeply_alias_fetch!(v)}
irb(main):005:2*   elsif x.instance_of? Array
irb(main):006:2*     x.each {|e| deeply_alias_fetch!(e)}
irb(main):007:1*   end
irb(main):008:0> end
=> :deeply_alias_fetch!
irb(main):009:0> h = {:a => {:b => 42}, :c => [{:d => 1, :e => 2}, {}]}
irb(main):010:0> deeply_alias_fetch!(h)
=> {:a=>{:b=>42}, :c=>[{:d=>1, :e=>2}, {}]}
irb(main):011:0> h[:a][:bb]
Traceback (most recent call last):
        5: from /usr/bin/irb:23:in `<main>'
        4: from /usr/bin/irb:23:in `load'
        3: from /usr/lib/ruby/gems/2.7.0/gems/irb-1.2.1/exe/irb:11:in `<top (required)>'
        2: from (irb):11
        1: from (irb):11:in `fetch'
KeyError (key not found: :bb)
Did you mean?  :b
irb(main):012:0> h[:c][0][:e]
=> 2
irb(main):013:0> h[:c][0][:f]
Traceback (most recent call last):
        5: from /usr/bin/irb:23:in `<main>'
        4: from /usr/bin/irb:23:in `load'
        3: from /usr/lib/ruby/gems/2.7.0/gems/irb-1.2.1/exe/irb:11:in `<top (required)>'
        2: from (irb):14
        1: from (irb):14:in `fetch'
KeyError (key not found: :f)
ggorlen
  • 44,755
  • 7
  • 76
  • 106
-4

if you can

use:

hash[["ayy","bee"]]

instead of:

hash["ayy"]["bee"]

it'll save a lot of annoyances

runub
  • 189
  • 8
  • I'm just amazed people use the same words as I do when I make test hashes x'D `{a: 'ayee', b: 'bee', c: 'sea'}` It only makes sense though... The singularity is upon us! – CTS_AE Mar 26 '19 at 22:55
  • For this to work, you'd need to use lists as keys: `h = {["ayy", "bee"] => 42} ; h[["ayy", "bee"]]`. It doesn't really help with using `fetch` to raise on missing keys as OP seems to be asking though. – ggorlen Jun 24 '21 at 17:44