Project

General

Profile

Actions

Feature #11643

closed

A new method on Hash to grab values out of nested hashes, failing gracefully

Added by gkop (Gabe Kopley) almost 10 years ago. Updated almost 10 years ago.

Status:
Closed
Assignee:
-
Target version:
-
[ruby-core:71293]

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

[2] http://stackoverflow.com/a/1820492/283398

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 sure dig 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 •
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 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

Actions #11

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]
Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0Like0