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)