Project

General

Profile

Actions

Feature #21545

open

#try_dig, a dig that returns early if it cannot dig deeper

Added by cb341 (Daniel Bengl) about 1 month ago. Updated 22 days ago.

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

Description

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:

{ 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:

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, #15563) by covering the tolerant lookup case.

A possible sketch:

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) 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
Actions #1

Updated by cb341 (Daniel Bengl) about 1 month ago

  • Description updated (diff)
Actions #2

Updated by cb341 (Daniel Bengl) about 1 month ago

  • Description updated (diff)
Actions #3

Updated by cb341 (Daniel Bengl) about 1 month ago

  • Subject changed from `#try_dig`: a dig that returns early if it cannot dig deeper to #try_dig, a dig that returns early if it cannot dig deeper

Updated by matheusrich (Matheus Richard) about 1 month ago

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.

Updated by matz (Yukihiro Matsumoto) 27 days ago

How about adding keyword argument, e.g. exception: true to behave try_dig.

Matz.

Updated by Dan0042 (Daniel DeLorme) 27 days ago

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?

Updated by herwin (Herwin W) 26 days ago

Maybe I'm reading too much into the examples, but this looks to me like something where pattern matching would be more suitable:

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

Updated by cb341 (Daniel Bengl) 22 days ago

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.

Updated by cb341 (Daniel Bengl) 22 days ago

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?

Updated by cb341 (Daniel Bengl) 22 days ago

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?:

 # 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/core_ext/object/blank.rb#L22C1-L27C6

Also if dig! would be introduced (#12282, #15563), it'd make sense to also introduce a rescue behaving dig.

Updated by cb341 (Daniel Bengl) 22 days ago

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:

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.

Updated by Dan0042 (Daniel DeLorme) 22 days ago

  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.

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like1Like1Like1Like0Like0Like0Like0Like0