Project

General

Profile

Actions

Feature #12282

open

Hash#dig! for repeated applications of Hash#fetch

Added by robb (Robb Shecter) over 9 years ago. Updated 1 day ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:74946]

Description

A new feature for your consideration: #dig! which is to #fetch as #dig is to #[]. For me and maybe many others, Hash#fetch is used much more than Hash#[]. And traversing multiple fetches isn't very convenient nor Ruby-like, e.g.: places.fetch(:countries).fetch(:canada).fetch(ontario).

Here's how it would work:

places = { countries: { canada: true } }

places.dig  :countries, :canada  # => true
places.dig! :countries, :canada  # => true

places.dig  :countries, :canada, :ontario  # => nil
places.dig! :countries, :canada, :ontario  # => KeyError: Key not found: :ontario

Here's an implementation and tests: https://gist.github.com/dogweather/819ccdb41c9db0514c163cfdb1c528e2


Related issues 3 (2 open1 closed)

Has duplicate Ruby - Feature #15563: #dig that throws an exception if a key doesn't existOpenActions
Has duplicate Ruby - Feature #14602: Version of dig that raises error if a key is not presentOpenActions
Has duplicate Ruby - Feature #20815: Fetch for nested hash ClosedActions

Updated by sawa (Tsuyoshi Sawada) over 9 years ago

This makes sense only within limited cases, i.e. when the same key never appears at different depths. For example, if you get an error:

hash.dig!(:foo, :bar, :foo) # => KeyError: Key not found: :foo

you cannot tell whether the :foo at the first depth or the third depth (or both) is missing. In such case, there is not much difference from doing:

hash[:foo][:bar][:foo] # => NoMethodError: undefined method `[]' for nil:NilClass

from the point of view of information the error provides. (With dig!, all you can tell is that the error was not caused by :bar.) I do not see much value in having a method for such limited use case.

Updated by nobu (Nobuyoshi Nakada) over 9 years ago

  • Description updated (diff)

I'm negative because:

  1. This example is wrong.

    places.dig  :countries, :canada, :ontario  # => nil
    

    It raises a TypeError.

  2. It feels curious to me that the method with '!' raises an exception whereas the method without '!' doesn't.

Updated by nobu (Nobuyoshi Nakada) over 9 years ago

Nobuyoshi Nakada wrote:

It raises a TypeError.

So you have the method which raises an exception already.

Updated by sawa (Tsuyoshi Sawada) over 9 years ago

Nobuyoshi Nakada wrote:

It raises a TypeError.

I think it is a typographical error of

places.dig  :countries, :ontario # => nil
places.dig! :countries, :ontario # => KeyError: Key not found: :ontario

Updated by shyouhei (Shyouhei Urabe) over 9 years ago

I don't like the name. It doesn't uniform other usage of bang in method names.

Updated by shevegen (Robert A. Heiler) over 9 years ago

I concur with Shyouhei Urabe - the name seems to not entirely fit the
given outcome.

More typical use cases of methods with ! bang, if we ignore any
exception, would be more akin to things such as:

x = "abc"       # => "abc"
x.delete 'c'    # => "ab"
x               # => "abc"
x.delete! 'c'   # => "ab"
x               # => "ab"

On the topic of hashes as data structures, assumingly that they
may be more nested than the usual array, I tend to always attempt
to have all hashes and arrays as simple as possible, if and when
that is possible (it is not always possible, see the older
discussions about before .dig was added; but dig! is a weird name,
we want to obtain something, not change the data structure right?).

Updated by k0kubun (Takashi Kokubun) about 8 years ago

How about this name?

places.deep_fetch(:countries, :canada, :ontario)

I've encountered the case which I did "places.fetch(:countries).fetch(:canada).fetch(:ontario)" multiple times. I want this method.

Updated by robb (Robb Shecter) over 6 years ago

Thanks everyone, for the discussion. I realize that my original comparison with #dig had a typographical error. Here's a gem implementation, with examples that are correct: https://github.com/dogweather/digbang

require 'dig_bang'

places = {
  world: {
    uk: true,
    usa: true
  }
}

# No difference when the key exists
places.dig  :world, :uk # true
places.dig! :world, :uk # true

# A relevant error when the key is missing
places.dig  :world, :uk, :alaska # nil
places.dig! :world, :uk, :alaska # KeyError: Key not found: :alaska

About the method name with the bang. I see that this might be more of a Rails naming convention, which ! methods perform the same action, but throw an error instead of returning a nil on failure. And that's exactly my intent with dig! vs. dig. Basically, a "checked" dig. I don't think that Ruby has a naming convention for an alternate interface which throws an exception vs. return a nil. (?)

module DigBang
  def self.fetch_all(fetchable, keys)
    keys.reduce(fetchable) { |a, e| a.fetch(e) }
  end
end

class Hash
  def dig!(*keys)
    DigBang.fetch_all(self, keys)
  end
end

class Array
  def dig!(*keys)
    DigBang.fetch_all(self, keys)
  end
end

Updated by robb (Robb Shecter) over 6 years ago

Another naming idea is #fetch_all, signalling that this is essentially a #fetch over a list of keys.

Actions #10

Updated by k0kubun (Takashi Kokubun) over 6 years ago

  • Has duplicate Feature #15563: #dig that throws an exception if a key doesn't exist added
Actions #11

Updated by k0kubun (Takashi Kokubun) over 6 years ago

  • Has duplicate Feature #14602: Version of dig that raises error if a key is not present added
Actions #12

Updated by alanwu (Alan Wu) 11 months ago

Updated by briankung (Brian Kung) 25 days ago

Agreed, this feature would be useful. I would like to essentially assert that all of the nodes along the path are found.

To that end, I would suggest calling it something like #fetch_path:

object = {foo: {bar: { baz: true}}

object.fetch_path(:foo, :bar, :baz) #=> true
object.fetch_path(:foo, :baz, :bar) #=> (irb):3:in 'Hash#fetch_path': key not found: :baz (KeyError)

Updated by matz (Yukihiro Matsumoto) 22 days ago

fetch_path is much better than dig! since this bang method is against our naming convention (if we have x and x!, a method with ! (bang) should be more dangerous).
At the same time, _path usually means file paths in Ruby, and possibly cause confusion, although I understand path here is totally valid.

Matz.

Updated by Dan0042 (Daniel DeLorme) 18 days ago

k0kubun (Takashi Kokubun) wrote in #note-7:

How about this name?

places.deep_fetch(:countries, :canada, :ontario)

Best suggestion I've seen so far.

Updated by Eregon (Benoit Daloze) 18 days ago

Agreed deep_fetch is good.

Also wanted to mention dig_fetch from https://bugs.ruby-lang.org/issues/14602#note-16, there is even a blog post about it so I think that illustrates it's a common name for this functionality.
What this issue wants is dig with fetch semantics instead of [] semantics, hence dig_fetch.

fetch_path doesn't seem ideal to me because this method would also be used e.g. with arrays and object.fetch_path(:foo, 2, :baz) doesn't really sounds like a "path".
Similarly, deep_fetch looks nice but doesn't make the relation to dig as clear as dig_fetch.

It could be obj.dig(:foo, 2, :baz, exception: true) I guess, but that's very verbose.

Updated by zverok (Victor Shepelev) 17 days ago

I'll allow myself to copy-paste my reasoning from the related ticket #14602:

Just a bit of "design space" analysis:

  1. I think dig! is unusual for core Ruby. A lot of Rubyists are used that in Rails pairs like find_by/find_by! are raising/non-raising, but I don't remember any Ruby core API using this convention
  2. I don't believe the keyword argument is expressive enough. The "visual structure" of the dig signature includes multiple values of user data (and the list of values might be of arbitrary length), so the option at the end of arguments is a) not visible enough and b) not immediately intuitively obvious if it isn't part of user's data
  3. In Hash, we already have at least two examples of using fetch in a sense "get the value or fail": #[] vs #fetch and #values_at vs #fetch_values. It seems like it gives enough precedent to look at the "fetch"-based naming, and it seems like fetch_dig, while grammatically not ideal, would be guessable enough, based on existing experience.

(...)

I fully agree with @duerst (Martin Dürst) in #14602#note-24:

maybe we can think it as a combination of dig with fetch. Then what about dig_fetch or fetch_dig? These names don't look very natural, but it's easy to understand what they are about.

First, we already have examples of fetch-based naming: not only #fetch itself as a variation of #[], but also #fetch_values as a variation of #values_at, so there is a precedent for recognizability

Second, I value short one-word names, so all the witty options like #shovel and #retrieve are nice, but I am afraid that when we have a variation of a known method in an API established long ago, introducing completely new word into Ruby would be a false move. Imagine you started to read code and met with #retrieve (or #shovel) for the first time. There is nothing that might help you to understand what it does; one verb that "a bit resembles dig" is not suggestive enough.

Third, deep_fetch is somewhat suggestive, but the problem "it behaves like dig, but the name logic is nothing like dig" stands. Maybe if it would a pair of, IDK, #deep_fetch and #deep_get it might've been tolerable, but now is too late for that, everybody has used to #dig.

fetch_dig, OTOH, is reasonably short, clearly suggests the meaning, and follows the logic of other methods existing.

Updated by briankung (Brian Kung) 1 day ago · Edited

How about dig_cons(), as in "dig consecutive," similar to each_cons()? They'd be different in that the argument to each_cons is how large each output slice is, whereas the arguments to dig_cons would represent consecutive keys in a "path" (not the file path, of course). The similarity would be the idea of "consecutive" elements.

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0Like1Like0Like0Like0Like0Like0Like0Like0Like0Like0Like1Like0