[ruby-core:122969] [Ruby Feature#21545] `#try_dig`: a dig that returns early if it cannot dig deeper

Issue #21545 has been reported by cb341 (Daniel Bengl). ---------------------------------------- Feature #21545: `#try_dig`: a dig that returns early if it cannot dig deeper https://bugs.ruby-lang.org/issues/21545 * Author: cb341 (Daniel Bengl) * Status: Open ---------------------------------------- Ruby offers `dig` for traversing nested hashes and arrays. It is strict and will raise if an intermediary object does not support `dig`. In many cases we only want to attempt the lookup and return `nil` if it cannot be followed, without caring about the exact reason. **Example:** ```rb { a: "foo" }.dig(:a, :b) # TypeError: String does not have #dig method { a: "foo" }.try_dig(:a, :b) # => nil ``` This is especially useful when dealing with data from APIs or other inconsistent sources: ```rb api_response = { status: "ok" } api_response.try_dig(:status, :code) # => nil api_response = { status: { code: 200 } } api_response.try_dig(:status, :code) # => 200 ``` The name `try_dig` makes it clear that it behaves like `dig` but will never raise for structural mismatches. It complements `dig` and the proposed `dig!` ([#12282](https://bugs.ruby-lang.org/issues/12282), [#15563](https://bugs.ruby-lang.org/issues/15563)) by covering the tolerant lookup case. A possible sketch: ```rb class Object def try_dig(*path) current = self path.each do |key| return nil unless current.respond_to?(:dig) begin current = current.dig(key) rescue StandardError return nil end end current end end ``` **Advantages** - Simplifies tolerant lookups without repetitive rescue logic - Clear intent when the value is optional - Useful for working with inconsistent or partially known data structures - Complements `dig` and potential `dig!` by covering the tolerant case **Disatvantages** - May hide structural issues that should be noticed during development -- https://bugs.ruby-lang.org/

Issue #21545 has been updated by matheusrich (Matheus Richard). Just want to point out that Ruby itself doesn't have any concept of `try`. This *might* be best as a proposal for Active Support. ---------------------------------------- Feature #21545: #try_dig, a dig that returns early if it cannot dig deeper https://bugs.ruby-lang.org/issues/21545#change-114280 * Author: cb341 (Daniel Bengl) * Status: Open ---------------------------------------- Ruby offers `dig` for traversing nested hashes and arrays. It is strict and will raise if an intermediary object does not support `dig`. In many cases we only want to attempt the lookup and return `nil` if it cannot be followed, without caring about the exact reason. **Example:** ```rb { a: "foo" }.dig(:a, :b) # TypeError: String does not have #dig method { a: "foo" }.try_dig(:a, :b) # => nil ``` This is especially useful when dealing with data from APIs or other inconsistent sources: ```rb api_response = { status: "ok" } api_response.try_dig(:status, :code) # => nil api_response = { status: { code: 200 } } api_response.try_dig(:status, :code) # => 200 ``` The name `try_dig` makes it clear that it behaves like `dig` but will never raise for structural mismatches. It complements `dig` and the proposed `dig!` ([#12282](https://bugs.ruby-lang.org/issues/12282), [#15563](https://bugs.ruby-lang.org/issues/15563)) by covering the tolerant lookup case. A possible sketch: ```rb class Object def try_dig(*path) current = self path.each do |key| return nil unless current.respond_to?(:dig) begin current = current.dig(key) rescue StandardError return nil end end current end end ``` _I initially proposed this in Ruby core ([PR #14203](https://github.com/ruby/ruby/pull/14203)) and even implemented it in C, but later realized that if this gets introduced at all it probably makes more sense to have it in ActiveSupport rather than in core Ruby._ **Advantages** - Simplifies tolerant lookups without repetitive rescue logic - Clear intent when the value is optional - Useful for working with inconsistent or partially known data structures - Complements `dig` and potential `dig!` by covering the tolerant case **Disatvantages** - May hide structural issues that should be noticed during development -- https://bugs.ruby-lang.org/

Issue #21545 has been updated by matz (Yukihiro Matsumoto). How about adding keyword argument, e.g. `exception: true` to behave `try_dig`. Matz. ---------------------------------------- Feature #21545: #try_dig, a dig that returns early if it cannot dig deeper https://bugs.ruby-lang.org/issues/21545#change-114309 * Author: cb341 (Daniel Bengl) * Status: Open ---------------------------------------- Ruby offers `dig` for traversing nested hashes and arrays. It is strict and will raise if an intermediary object does not support `dig`. In many cases we only want to attempt the lookup and return `nil` if it cannot be followed, without caring about the exact reason. **Example:** ```rb { a: "foo" }.dig(:a, :b) # TypeError: String does not have #dig method { a: "foo" }.try_dig(:a, :b) # => nil ``` This is especially useful when dealing with data from APIs or other inconsistent sources: ```rb api_response = { status: "ok" } api_response.try_dig(:status, :code) # => nil api_response = { status: { code: 200 } } api_response.try_dig(:status, :code) # => 200 ``` The name `try_dig` makes it clear that it behaves like `dig` but will never raise for structural mismatches. It complements `dig` and the proposed `dig!` ([#12282](https://bugs.ruby-lang.org/issues/12282), [#15563](https://bugs.ruby-lang.org/issues/15563)) by covering the tolerant lookup case. A possible sketch: ```rb class Object def try_dig(*path) current = self path.each do |key| return nil unless current.respond_to?(:dig) begin current = current.dig(key) rescue StandardError return nil end end current end end ``` _I initially proposed this in Ruby core ([PR #14203](https://github.com/ruby/ruby/pull/14203)) and even implemented it in C, but later realized that if this gets introduced at all it probably makes more sense to have it in ActiveSupport rather than in core Ruby._ **Advantages** - Simplifies tolerant lookups without repetitive rescue logic - Clear intent when the value is optional - Useful for working with inconsistent or partially known data structures - Complements `dig` and potential `dig!` by covering the tolerant case **Disatvantages** - May hide structural issues that should be noticed during development -- https://bugs.ruby-lang.org/

Issue #21545 has been updated by Dan0042 (Daniel DeLorme). I have never seen an API that can return either a string `"ok"` or a hash `{ code: 200 }` Swallowing exceptions like that seems very dangerous to me, not a pattern we should have in ruby core. It seems to be functionally the same as `api_response.dig(:status, :code) rescue nil`, what's the difference or advantage? ---------------------------------------- Feature #21545: #try_dig, a dig that returns early if it cannot dig deeper https://bugs.ruby-lang.org/issues/21545#change-114349 * Author: cb341 (Daniel Bengl) * Status: Open ---------------------------------------- Ruby offers `dig` for traversing nested hashes and arrays. It is strict and will raise if an intermediary object does not support `dig`. In many cases we only want to attempt the lookup and return `nil` if it cannot be followed, without caring about the exact reason. **Example:** ```rb { a: "foo" }.dig(:a, :b) # TypeError: String does not have #dig method { a: "foo" }.try_dig(:a, :b) # => nil ``` This is especially useful when dealing with data from APIs or other inconsistent sources: ```rb api_response = { status: "ok" } api_response.try_dig(:status, :code) # => nil api_response = { status: { code: 200 } } api_response.try_dig(:status, :code) # => 200 ``` The name `try_dig` makes it clear that it behaves like `dig` but will never raise for structural mismatches. It complements `dig` and the proposed `dig!` ([#12282](https://bugs.ruby-lang.org/issues/12282), [#15563](https://bugs.ruby-lang.org/issues/15563)) by covering the tolerant lookup case. A possible sketch: ```rb class Object def try_dig(*path) current = self path.each do |key| return nil unless current.respond_to?(:dig) begin current = current.dig(key) rescue StandardError return nil end end current end end ``` _I initially proposed this in Ruby core ([PR #14203](https://github.com/ruby/ruby/pull/14203)) and even implemented it in C, but later realized that if this gets introduced at all it probably makes more sense to have it in ActiveSupport rather than in core Ruby._ **Advantages** - Simplifies tolerant lookups without repetitive rescue logic - Clear intent when the value is optional - Useful for working with inconsistent or partially known data structures - Complements `dig` and potential `dig!` by covering the tolerant case **Disatvantages** - May hide structural issues that should be noticed during development -- https://bugs.ruby-lang.org/

Issue #21545 has been updated by herwin (Herwin W). Maybe I'm reading too much into the examples, but this looks to me like something where pattern matching would be more suitable: ```ruby def match(input) case input in { status: { code: Integer => status } } puts "Status code: #{status}" in { status: status } puts "Other status: #{status}" else raise "mismatch: #{input}" end end match({ status: "ok" }) match({ status: { code: 200 } }) ``` Output: ``` Other status: ok Status code: 200 ``` ---------------------------------------- Feature #21545: #try_dig, a dig that returns early if it cannot dig deeper https://bugs.ruby-lang.org/issues/21545#change-114373 * Author: cb341 (Daniel Bengl) * Status: Open ---------------------------------------- Ruby offers `dig` for traversing nested hashes and arrays. It is strict and will raise if an intermediary object does not support `dig`. In many cases we only want to attempt the lookup and return `nil` if it cannot be followed, without caring about the exact reason. **Example:** ```rb { a: "foo" }.dig(:a, :b) # TypeError: String does not have #dig method { a: "foo" }.try_dig(:a, :b) # => nil ``` This is especially useful when dealing with data from APIs or other inconsistent sources: ```rb api_response = { status: "ok" } api_response.try_dig(:status, :code) # => nil api_response = { status: { code: 200 } } api_response.try_dig(:status, :code) # => 200 ``` The name `try_dig` makes it clear that it behaves like `dig` but will never raise for structural mismatches. It complements `dig` and the proposed `dig!` ([#12282](https://bugs.ruby-lang.org/issues/12282), [#15563](https://bugs.ruby-lang.org/issues/15563)) by covering the tolerant lookup case. A possible sketch: ```rb class Object def try_dig(*path) current = self path.each do |key| return nil unless current.respond_to?(:dig) begin current = current.dig(key) rescue StandardError return nil end end current end end ``` _I initially proposed this in Ruby core ([PR #14203](https://github.com/ruby/ruby/pull/14203)) and even implemented it in C, but later realized that if this gets introduced at all it probably makes more sense to have it in ActiveSupport rather than in core Ruby._ **Advantages** - Simplifies tolerant lookups without repetitive rescue logic - Clear intent when the value is optional - Useful for working with inconsistent or partially known data structures - Complements `dig` and potential `dig!` by covering the tolerant case **Disatvantages** - May hide structural issues that should be noticed during development -- https://bugs.ruby-lang.org/

Issue #21545 has been updated by cb341 (Daniel Bengl). matheusrich (Matheus Richard) wrote in #note-4:
Just want to point out that Ruby itself doesn't have any concept of `try`. This *might* be best as a proposal for Active Support.
It does make more sense to have this part of Active Support. Thanks for the suggestion, matheusrich. ---------------------------------------- Feature #21545: #try_dig, a dig that returns early if it cannot dig deeper https://bugs.ruby-lang.org/issues/21545#change-114384 * Author: cb341 (Daniel Bengl) * Status: Open ---------------------------------------- Ruby offers `dig` for traversing nested hashes and arrays. It is strict and will raise if an intermediary object does not support `dig`. In many cases we only want to attempt the lookup and return `nil` if it cannot be followed, without caring about the exact reason. **Example:** ```rb { a: "foo" }.dig(:a, :b) # TypeError: String does not have #dig method { a: "foo" }.try_dig(:a, :b) # => nil ``` This is especially useful when dealing with data from APIs or other inconsistent sources: ```rb api_response = { status: "ok" } api_response.try_dig(:status, :code) # => nil api_response = { status: { code: 200 } } api_response.try_dig(:status, :code) # => 200 ``` The name `try_dig` makes it clear that it behaves like `dig` but will never raise for structural mismatches. It complements `dig` and the proposed `dig!` ([#12282](https://bugs.ruby-lang.org/issues/12282), [#15563](https://bugs.ruby-lang.org/issues/15563)) by covering the tolerant lookup case. A possible sketch: ```rb class Object def try_dig(*path) current = self path.each do |key| return nil unless current.respond_to?(:dig) begin current = current.dig(key) rescue StandardError return nil end end current end end ``` _I initially proposed this in Ruby core ([PR #14203](https://github.com/ruby/ruby/pull/14203)) and even implemented it in C, but later realized that if this gets introduced at all it probably makes more sense to have it in ActiveSupport rather than in core Ruby._ **Advantages** - Simplifies tolerant lookups without repetitive rescue logic - Clear intent when the value is optional - Useful for working with inconsistent or partially known data structures - Complements `dig` and potential `dig!` by covering the tolerant case **Disatvantages** - May hide structural issues that should be noticed during development -- https://bugs.ruby-lang.org/

Issue #21545 has been updated by cb341 (Daniel Bengl). matz (Yukihiro Matsumoto) wrote in #note-5:
How about adding keyword argument, e.g. `exception: true` to behave `try_dig`.
Matz.
Why would you add the option? The thought behind `try_dig` is that there is no exception when you dig too deep. The exception behavior already exists in `dig` so you'd suggest adding `exception: false` to `dig`? ---------------------------------------- Feature #21545: #try_dig, a dig that returns early if it cannot dig deeper https://bugs.ruby-lang.org/issues/21545#change-114385 * Author: cb341 (Daniel Bengl) * Status: Open ---------------------------------------- Ruby offers `dig` for traversing nested hashes and arrays. It is strict and will raise if an intermediary object does not support `dig`. In many cases we only want to attempt the lookup and return `nil` if it cannot be followed, without caring about the exact reason. **Example:** ```rb { a: "foo" }.dig(:a, :b) # TypeError: String does not have #dig method { a: "foo" }.try_dig(:a, :b) # => nil ``` This is especially useful when dealing with data from APIs or other inconsistent sources: ```rb api_response = { status: "ok" } api_response.try_dig(:status, :code) # => nil api_response = { status: { code: 200 } } api_response.try_dig(:status, :code) # => 200 ``` The name `try_dig` makes it clear that it behaves like `dig` but will never raise for structural mismatches. It complements `dig` and the proposed `dig!` ([#12282](https://bugs.ruby-lang.org/issues/12282), [#15563](https://bugs.ruby-lang.org/issues/15563)) by covering the tolerant lookup case. A possible sketch: ```rb class Object def try_dig(*path) current = self path.each do |key| return nil unless current.respond_to?(:dig) begin current = current.dig(key) rescue StandardError return nil end end current end end ``` _I initially proposed this in Ruby core ([PR #14203](https://github.com/ruby/ruby/pull/14203)) and even implemented it in C, but later realized that if this gets introduced at all it probably makes more sense to have it in ActiveSupport rather than in core Ruby._ **Advantages** - Simplifies tolerant lookups without repetitive rescue logic - Clear intent when the value is optional - Useful for working with inconsistent or partially known data structures - Complements `dig` and potential `dig!` by covering the tolerant case **Disatvantages** - May hide structural issues that should be noticed during development -- https://bugs.ruby-lang.org/

Issue #21545 has been updated by cb341 (Daniel Bengl). Dan0042 (Daniel DeLorme) wrote in #note-6:
I have never seen an API that can return either a string `"ok"` or a hash `{ code: 200 }`
Maybe the API status example is not the best suited one for `try_dig`. It has been my observation that in certain cases APIs can return various types on a given path. `try_dig` would be used in the case that you are only concerned by a specific case, where the long path exists and you can dig while dismissing the other cases.
Swallowing exceptions like that seems very dangerous to me, not a pattern we should have in ruby core.
I agree with you that Exception swallowing in the core might not be the best idea, so I'll rather propose this to ActiveSupport.
It seems to be functionally the same as `api_response.dig(:status, :code) rescue nil`...
After further examination my proposed implementation of `rescue StandardError`, `return nil` is functionally identical to `rescue nil`.
...what's the difference or advantage? Why shouldn't we have an alias for this behavior? ActiveSupport defines many aliases such as present?:
```rb # An object is present if it's not blank. # # @return [true, false] def present? !blank? end ``` https://github.com/rails/rails/blob/main/activesupport/lib/active_support/co... Also if `dig!` would be introduced (#12282, #15563), it'd make sense to also introduce a rescue behaving `dig`. ---------------------------------------- Feature #21545: #try_dig, a dig that returns early if it cannot dig deeper https://bugs.ruby-lang.org/issues/21545#change-114386 * Author: cb341 (Daniel Bengl) * Status: Open ---------------------------------------- Ruby offers `dig` for traversing nested hashes and arrays. It is strict and will raise if an intermediary object does not support `dig`. In many cases we only want to attempt the lookup and return `nil` if it cannot be followed, without caring about the exact reason. **Example:** ```rb { a: "foo" }.dig(:a, :b) # TypeError: String does not have #dig method { a: "foo" }.try_dig(:a, :b) # => nil ``` This is especially useful when dealing with data from APIs or other inconsistent sources: ```rb api_response = { status: "ok" } api_response.try_dig(:status, :code) # => nil api_response = { status: { code: 200 } } api_response.try_dig(:status, :code) # => 200 ``` The name `try_dig` makes it clear that it behaves like `dig` but will never raise for structural mismatches. It complements `dig` and the proposed `dig!` ([#12282](https://bugs.ruby-lang.org/issues/12282), [#15563](https://bugs.ruby-lang.org/issues/15563)) by covering the tolerant lookup case. A possible sketch: ```rb class Object def try_dig(*path) current = self path.each do |key| return nil unless current.respond_to?(:dig) begin current = current.dig(key) rescue StandardError return nil end end current end end ``` _I initially proposed this in Ruby core ([PR #14203](https://github.com/ruby/ruby/pull/14203)) and even implemented it in C, but later realized that if this gets introduced at all it probably makes more sense to have it in ActiveSupport rather than in core Ruby._ **Advantages** - Simplifies tolerant lookups without repetitive rescue logic - Clear intent when the value is optional - Useful for working with inconsistent or partially known data structures - Complements `dig` and potential `dig!` by covering the tolerant case **Disatvantages** - May hide structural issues that should be noticed during development -- https://bugs.ruby-lang.org/

Issue #21545 has been updated by cb341 (Daniel Bengl). herwin (Herwin W) wrote in #note-7:
Maybe I'm reading too much into the examples, but this looks to me like something where pattern matching would be more suitable:
```ruby def match(input) case input in { status: { code: Integer => status } } puts "Status code: #{status}" in { status: status } puts "Other status: #{status}" else raise "mismatch: #{input}" end end
match({ status: "ok" }) match({ status: { code: 200 } }) ```
Output: ``` Other status: ok Status code: 200 ```
This is a beautiful solution to the API problem. I see how this can be better than `try_dig` as you can explicitly match all formats you want to handle and group the rest. Instead of raising you could also return nil if you only care about the data if it is present and don't care about edge cases. ---------------------------------------- Feature #21545: #try_dig, a dig that returns early if it cannot dig deeper https://bugs.ruby-lang.org/issues/21545#change-114387 * Author: cb341 (Daniel Bengl) * Status: Open ---------------------------------------- Ruby offers `dig` for traversing nested hashes and arrays. It is strict and will raise if an intermediary object does not support `dig`. In many cases we only want to attempt the lookup and return `nil` if it cannot be followed, without caring about the exact reason. **Example:** ```rb { a: "foo" }.dig(:a, :b) # TypeError: String does not have #dig method { a: "foo" }.try_dig(:a, :b) # => nil ``` This is especially useful when dealing with data from APIs or other inconsistent sources: ```rb api_response = { status: "ok" } api_response.try_dig(:status, :code) # => nil api_response = { status: { code: 200 } } api_response.try_dig(:status, :code) # => 200 ``` The name `try_dig` makes it clear that it behaves like `dig` but will never raise for structural mismatches. It complements `dig` and the proposed `dig!` ([#12282](https://bugs.ruby-lang.org/issues/12282), [#15563](https://bugs.ruby-lang.org/issues/15563)) by covering the tolerant lookup case. A possible sketch: ```rb class Object def try_dig(*path) current = self path.each do |key| return nil unless current.respond_to?(:dig) begin current = current.dig(key) rescue StandardError return nil end end current end end ``` _I initially proposed this in Ruby core ([PR #14203](https://github.com/ruby/ruby/pull/14203)) and even implemented it in C, but later realized that if this gets introduced at all it probably makes more sense to have it in ActiveSupport rather than in core Ruby._ **Advantages** - Simplifies tolerant lookups without repetitive rescue logic - Clear intent when the value is optional - Useful for working with inconsistent or partially known data structures - Complements `dig` and potential `dig!` by covering the tolerant case **Disatvantages** - May hide structural issues that should be noticed during development -- https://bugs.ruby-lang.org/

Issue #21545 has been updated by Dan0042 (Daniel DeLorme).
```ruby else raise "mismatch: #{input}" ```
Sort of off-topic nitpick, but I feel this is an anti-pattern. Without an `else` clause, the default behavior is already to raise a NoMatchingPatternError. ---------------------------------------- Feature #21545: #try_dig, a dig that returns early if it cannot dig deeper https://bugs.ruby-lang.org/issues/21545#change-114388 * Author: cb341 (Daniel Bengl) * Status: Open ---------------------------------------- Ruby offers `dig` for traversing nested hashes and arrays. It is strict and will raise if an intermediary object does not support `dig`. In many cases we only want to attempt the lookup and return `nil` if it cannot be followed, without caring about the exact reason. **Example:** ```rb { a: "foo" }.dig(:a, :b) # TypeError: String does not have #dig method { a: "foo" }.try_dig(:a, :b) # => nil ``` This is especially useful when dealing with data from APIs or other inconsistent sources: ```rb api_response = { status: "ok" } api_response.try_dig(:status, :code) # => nil api_response = { status: { code: 200 } } api_response.try_dig(:status, :code) # => 200 ``` The name `try_dig` makes it clear that it behaves like `dig` but will never raise for structural mismatches. It complements `dig` and the proposed `dig!` ([#12282](https://bugs.ruby-lang.org/issues/12282), [#15563](https://bugs.ruby-lang.org/issues/15563)) by covering the tolerant lookup case. A possible sketch: ```rb class Object def try_dig(*path) current = self path.each do |key| return nil unless current.respond_to?(:dig) begin current = current.dig(key) rescue StandardError return nil end end current end end ``` _I initially proposed this in Ruby core ([PR #14203](https://github.com/ruby/ruby/pull/14203)) and even implemented it in C, but later realized that if this gets introduced at all it probably makes more sense to have it in ActiveSupport rather than in core Ruby._ **Advantages** - Simplifies tolerant lookups without repetitive rescue logic - Clear intent when the value is optional - Useful for working with inconsistent or partially known data structures - Complements `dig` and potential `dig!` by covering the tolerant case **Disatvantages** - May hide structural issues that should be noticed during development -- https://bugs.ruby-lang.org/
participants (5)
-
cb341 (Daniel Bengl)
-
Dan0042 (Daniel DeLorme)
-
herwin (Herwin W)
-
matheusrich (Matheus Richard)
-
matz (Yukihiro Matsumoto)