Feature #11643
closedA new method on Hash to grab values out of nested hashes, failing gracefully
Description
(I posted this to the mailing list last year [0] and received no response, but am inspired to file an issue here based on the positive reception to https://bugs.ruby-lang.org/issues/11537 )
This comes up sometimes in Rails programming [1]:
params[:order] && params[:order][:shipping_info] && params[:order][:shipping_info][:country]
or
params[:order][:shipping_info][:country] rescue nil
or
params.fetch(:order, {}).fetch(:shipping_info, {}).fetch(:country, nil)
What if Hash gave us a method to accomplish this more concisely and semantically?
Eg.
params.traverse_nested_hashes_and_return_nil_if_a_key_isnt_found(:order, :shipping_info, :country)
Or to take a nice method name suggestion [2]:
params.dig(:order, :shipping_info, :country)
Another example solution is https://github.com/intridea/hashie#deepfetch (Although I don't like "fetch" in this method name since it doesn't and can't take a default value as an argument like Hash#fetch does)
What do you all think?
[0] https://groups.google.com/forum/#!topic/ruby-core-google/guleNgEJWcM
[1]
https://groups.google.com/d/msg/rubyonrails-core/bOkvcvS3t_A/QXLEXwt9ivAJ
https://stackoverflow.com/questions/1820451/ruby-style-how-to-check-whether-a-nested-hash-element-exists
https://stackoverflow.com/questions/19115838/how-do-i-use-the-fetch-method-for-nested-hash
https://stackoverflow.com/questions/10130726/ruby-access-multidimensional-hash-and-avoid-access-nil-object
Updated by phluid61 (Matthew Kerwin) almost 10 years ago
How about:
params.?[:order].?[shipping_info].?[country]
Updated by gkop (Gabe Kopley) almost 10 years ago
Matthew Kerwin wrote:
How about:
params.?[:order].?[shipping_info].?[country]
Thanks Matthew, I'll be honest, I hadn't thought of that. There is a certain appeal in avoiding adding a new method on Hash. On the other hand, by adding a new method we can more easily and more beautifully do metaprogramming, use a potentially more concise expression, convey more rich semantics, and possibly reduce the number of method calls.
Updated by matz (Yukihiro Matsumoto) almost 10 years ago
I prefer method way to (already reverted) params.?[:order].?[:shipping_info].?[:country]
.
I am not sure dig
is the best name for it. It's short, concise thought.
Any other idea, anyone?
Matz.
Updated by Hanmac (Hans Mackowiak) almost 10 years ago
dam i begun to like that "params.?[:order]" bad that it got reverted :/
i think the problem is that it might parse "?[" as a char or something?
Updated by dsisnero (Dominic Sisneros) almost 10 years ago
Yukihiro Matsumoto wrote:
I prefer method way to (already reverted)
params.?[:order].?[:shipping_info].?[:country]
.
I am not suredig
is the best name for it. It's short, concise thought.
Any other idea, anyone?Matz.
clojure has get-in for their maps, how about fetch_in with replacement like fetch
hash.fetch_in(:order, :shipping_info, :country, 'Not found')
Updated by austin (Austin Ziegler) almost 10 years ago
The problem with hash.fetch_in(:order, :shipping_info, :country, 'Not found')
is that 'Not found'
is a (possibly) valid key. You would need to
implement this with a kwarg.
class Hash
def fetch_in(*keys, **kwargs, &block)
keys = keys.dup
ckey = keys.shift
unless self.key?(ckey)
return kwargs[:default] if kwargs.key?(:default)
return block.call(ckey) if block
fail KeyError, "key not found #{ckey.inspect}"
end
child = self[ckey]
if keys.empty?
child
elsif child.respond_to?(:fetch_in)
child.fetch_in(*keys, **kwargs, &block)
else
fail ArgumentError, 'more keys than Hashes'
end
end
end
a = {
a: {
b: {
c: :d
}
}
}
def y
yield
rescue => e
e
end
p y { a }
p y { a.fetch_in(:a) }
p y { a.fetch_in(:a, :b) }
p y { a.fetch_in(:a, :b, :c) }
p y { a.fetch_in(:a, :b, :c, :d) }
p y { a.fetch_in(:a, :b, :d) }
p y { a.fetch_in(:a, :b, :d, default: 'z') }
p y { a.fetch_in(:a, :b, :d) { 'z' } }
As a proposed name, I suggest locate
.
--
Austin Ziegler • halostatue@gmail.com • austin@halostatue.ca
http://www.halostatue.ca/ • http://twitter.com/halostatue
Updated by keithrbennett (Keith Bennett) almost 10 years ago
I like the 'dig' method approach for these reasons:
- it does not require any fanciness or magic that could confuse novice Rubyists
- it does not require any change to the interpreter
- the name 'dig' is concise and intention-revealing
I have been hoping for this feature for a long time. This would be great.
Updated by matz (Yukihiro Matsumoto) almost 10 years ago
The idea is accepted. The name is the problem. The current candidates are 'dig' and 'fetch_in'.
I prefer 'dig'. If you have any other idea, please propose.
Matz.
Updated by ko1 (Koichi Sasada) almost 10 years ago
Discussion: https://docs.google.com/document/d/1D0Eo5N7NE_unIySOKG9lVj_eyXf66BQPM4PKp7NvMyQ/pub
Feel free to continue discussion on this ticket.
Updated by Hanmac (Hans Mackowiak) almost 10 years ago
hm shortly patch idea: instead of
keys = keys.dup
ckey = keys.shift
wouldn't
ckey, *keys = keys
be better?
EDIT:
maybe a similar function does needed to add to Array too if there is a nested Array/Hash combination like from JSON
Updated by nobu (Nobuyoshi Nakada) almost 10 years ago
- Status changed from Open to Closed
Applied in changeset r52504.
dig
- array.c (rb_ary_dig): new method Array#dig.
- hash.c (rb_hash_dig): new method Hash#dig.
- object.c (rb_obj_dig): dig in nested arrays/hashes.
[Feature #11643]