Feature #12282
openHash#dig! for repeated applications of Hash#fetch
Added by robb (Robb Shecter) over 9 years ago. Updated about 17 hours ago.
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
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:
-
This example is wrong.
places.dig :countries, :canada, :ontario # => nil
It raises a
TypeError
. -
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.
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
Updated by k0kubun (Takashi Kokubun) about 6 years ago
- Has duplicate Feature #14602: Version of dig that raises error if a key is not present added
Updated by alanwu (Alan Wu) 10 months ago
- Has duplicate Feature #20815: Fetch for nested hash added
Updated by briankung (Brian Kung) 9 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) 6 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) 2 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) 1 day 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) about 17 hours ago
I'll allow myself to copy-paste my reasoning from the related ticket #14602:
Just a bit of "design space" analysis:
- I think
dig!
is unusual for core Ruby. A lot of Rubyists are used that in Rails pairs likefind_by
/find_by!
are raising/non-raising, but I don't remember any Ruby core API using this convention - 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 - 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 likefetch_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
withfetch
. Then what aboutdig_fetch
orfetch_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.