-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add dig method to Enumerable #3536
Conversation
Awesome 🎉 |
cb022ca
to
17e56f5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One big difference to the Ruby implementations is that it also accept Arrays, ie
{:x => [1,2,3]}.dig(:x, 2)
would return 3.
Conversely, #dig is also implemented on Arrays (and Ostructs and Structs, but I'm not certain how relevant those would be).
@@ -845,6 +876,14 @@ class Hash(K, V) | |||
raise "Hash table too big" | |||
end | |||
|
|||
private def dig_deep(hash_keys) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does extracting the implementation into a private method instead of putting it in the variant taking the array add anything?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See the has_key?
method. It references another private method that does more work.
My reasoning is that the deep_dig
method would be the workhorse but the other dig
and dig?
methods would be the public facing methods, making the code more DRY.
# h.get ["z"] # => nil | ||
# h.get [] of String # => nil | ||
# ``` | ||
def dig?(hash_keys : Array) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cannot Arrays also be hash keys? What would you assume
arr = [1]
h1 = {arr => 42}
puts h1.dig(arr)
to output?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ahh crap. ok, I'll have to rework it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there some kind of spec or anything that show what is a valid hash key?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I know, everything that responds to #hash.
I tried to do an implementation that matches the ruby behaviour, but I run into some strange compile error on HEAD. Works fine in Crystal 0.19.4:
(or well, the Ruby variant raises TypeError, but that doesn't exist here) |
@yxhuvud If we were to do recursion we wouldn't be able to have a single method that could handle both I would like to do some benchmarks between recursion and |
@samueleaton you could raise on |
@Sija But i would lose the meta information about which key caused the exception. raise KeyError.new "Missing hash key: #{key.inspect}" |
In order to get this working for hashes AND arrays I'm gonna refactor it to be recursive as @yxhuvud suggested. We can see about DRYing it up and refactoring afterword. |
i think I found a bug that is preventing recursion. My def dig?(k : K, *key_path)
val = fetch(k, nil)
return val if key_path.size == 0
return nil unless val.responds_to? :dig
val.dig?(*key_path)
end using the following hash:
If I give h.dig?("a", "b", "c") # => "d" But if the path is shorter than the longest path, it fails:
|
@samueleaton here is a functionalish implementation of class Hash(K, V)
def dig(key : K, *subkeys)
if (value = self[key]?) && value.responds_to?(:dig)
value.dig(*subkeys)
end
end
def dig(key : K)
self[key]?
end
end
class Array(T)
def dig(key : Int32, *subkeys)
if (value = self[key]?) && value.responds_to?(:dig)
value.dig(*subkeys)
end
end
def dig(key : Int32)
self[key]?
end
end
hsh = { "a" => { "b" => { "c" => 1 } } }
p hsh.dig("a")
p hsh.dig("a", "b")
p hsh.dig("a", "b", "c")
ary = [ [ [ 1, 2] ] ]
p ary.dig(0)
p ary.dig(0, 0)
p ary.dig(0, 0, 1)
hsh_ary = { "a" => { "b" => [1, 2, { "c" => [1, 2, 3] }, 4] } }
p hsh_ary.dig("a", "b", 2, "c") BTW I don't think there is much value in having both a |
66e0897
to
5aae264
Compare
@ysbaddaden that's elegant AF. You make it look easy haha. |
describe "dig" do | ||
it "gets the value at given path from given splat" do | ||
a = ["x", {"a" => ["b"]}] | ||
a.dig(1, "a", 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing .should ...
expectation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+5 pts to Sija. 👍
end | ||
end | ||
|
||
def dig(key : Int32) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isn't this definition redundant due to previously defined dig(key : T)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be resolved now
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My bad, sorry! In the end there might be second definition needed (dig(index : Int)
) due to []?(index : Int)
defined in Indexable(T)
module (included by Array(T)
), which does not take T
as an argument type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of my specs were still passing after I removed it. It should be fine.
@@ -509,6 +509,20 @@ describe "Array" do | |||
b.should eq(3) | |||
end | |||
|
|||
describe "dig" do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't all the specs be in spec/std/enumerable_spec.cr
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be resolved now
9c5736e
to
6782f59
Compare
@ysbaddaden Squashed it. Let me know if there anything else to do. 😊 Did you want to add this to the |
# h.dig "a", "b", 1 # => 20 | ||
# h.dig "a", "b", "c", "d", "e" # => nil | ||
# ``` | ||
def dig(key : K, *subkeys) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pardon if I just don't get the current state of things, but where is K
defined in Enumerable
? I thought that type placeholder names which weren't defined in context needed to be declared with forall
clauses.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So it should be like this, right?
def dig(key : K, *subkeys) forall K
if (value = self[key]?) && value.responds_to?(:dig)
value.dig(*subkeys)
end
end
def dig(key : K) forall K
self[key]?
end
This method doesn't fit into This method seems to belong to a module which assumes |
You're right @RX14, it doesn't belong to Enumerable. A |
Well, such a module would be Mappable rather than Diggable, surely? Also, Set would be an option as well, and if we get some Tree implementations, potentially that as well. But really, that is probably overthinking it as the common use case probably be to dig into JSON. |
If there were a Mappable module, with the key and value types as generic parameters, like Hash, surely it could be used for more than just dig in the future. If this method would merit a different category, others could fit in the same category. |
This kind of methods make a lot of sense in a dynamic language, but not so much in Crystal, unless type safety is preserved. For that, this should probably be a macro of some sort, I don't think it can be done with a single method. That's why I wouldn't add this method in the standard library. I'd also like to see some real use cases for this. |
@asterite The major benefit would be when working with JSON or YAML. Being able to traverse the depth of the object and either return the value or |
@asterite my use case (Hash only): configuring an application through a YAML (or JSON) file, where many optional components may have some configuration buried deep in hashes, or not (use defaults). Compare: config.dig("adapters", "http", name, "port")
# vs
config["adapters"].fetch("http", {}).fetch(name, {}).fetch("port", nil) I don't have a use case for digging inside arrays, thought. |
Given JSON:
To get the
Seeing as JSON can have arrays it would seem incomplete to not support |
As it stands it kinda competes with JSON.mapping. Perhaps the same could be achieved by doing something there? |
Although the specs passes checking for equality is half of the story.
Since checking for equality works smoothly with unions the returning type can go unnoticed. For the time being it will be better to leave this patterns in a shard and if end up been used a lot we can add it later. |
Is the union really that problematic? I like I won't create a Shard for such a small method (let's not go the npm way) but will most likely monkey-patch |
I agree with @ysbaddaden 👍 |
I vote to reopen this PR. |
If we reopen this:
The main usecase would be for traversing recursive aliases - in which case |
I don't understand why there should be both
I don't understand why this should be restricted to recursive types. Again, the whole point is to extract a value at any level in the hash. Of course you get an union and must deal with it. |
Sorry, I pushed wrong button. |
It's not going to be restricted to recursive types - I don't even know how that would be possible, I was just talking about the main usecase probably being And I don't understand why |
|
@ysbaddaden yes that makes sense if we keep |
Add the
dig
method as seen in Ruby's Hash#dig and Array#digAlso adds adig?
method that will not raise an error but will instead returnnil
if the key path doesn't exist.