Ruby 2.3 dig Method - Thoughts and Examples

Since Ruby 2.3 launched its new Hash#dig and Array#dig feature, it got my attention on how it would help “digging” unreliable objects. This incredible method makes you safely navigate through nested objects when dealing with third party APIs.

It also makes it makes easy to refactor projects using OpenStructs structures and replace those with hashes.

What it is?

The #dig is basically an elvis operator for hashes and arrays. It’ll retrieve a value from the array/hash, given a set of keys. If no value is found, it’ll return nil.

Ruby 2.3 also introduced a safe navigational operator, but I won’t be covering this since Giorgi Mitrev wrote a very good article about it.

Here are some practical examples on how you could be using this on your code:

Examples

Rails parameters

Whenever dealing with forms and their nested attributes, eventually, you’ll face nested hashes.

Let’s say you want to convert some user checkboxes values to integers:

# receiving params as `params = { user: { choices: ["1", "3", "5"] } }`
def coerce_user_choices
  choices = params.dig(:user, :choices) || []
  choices.map(&:to_i)
end

That’s nice! Way better than reading

params[:user] && params[:user][:choices] || []

Dealing with APIs

Imagine you’re receiving the following values from a JSON API:

{
  person: {
    attributes: {
      address: {
        street: "Street Foo",
        country: {
          name: "Country Bar",
        }
      }
    }
  }
}

With #dig, you can navigate through it without worrying about nils or invalid attributes, allowing you to create a wrapper around this response object.

Let’s say you wanted to select some of the JSON response data and format it to present to your view layer:

module Presenter
  class Person
    attr_reader :json

    PERSON_ATTRS  = [:person, :attributes]
    COUNTRY_NAME  = PERSON_ATTRS + [:address, :country, :name]
    STREET_NAME   = PERSON_ATTRS + [:address, :street]

    def initialize(person_json)
      @json = person_json
    end

    def country
      json.dig(*COUNTRY_NAME)
    end

    def street
      json.dig(*STREET_NAME)
    end
  end
end

presenter = Presenter::Person.new(person_json)
presenter.country # => "Country Bar"
presenter.street  # => "Street Foo"

Easy, isn’t it? Way nicer than using a bunch of ActiveSupport#trys everywhere. Better, It’s just Ruby!

Refactoring OpenStructs

Couple of years back, I’ve inherited a project with a bunch of nested OpenStructs. The good part on OpenStruct is also its biggest disadvantage: it allows flexible attributes, making objects completely unpredictable.

For the sake of simplicity, let’s say you have the following small scenario:

require 'ostruct'

class Country
  def self.read
    OpenStruct.new({
      name: "Country Bar"
    })
  end
end

class Address
  def self.read
    OpenStruct.new({
      country: Country.read
    })
  end
end

class Person < OpenStruct
  def self.read
    OpenStruct.new({
      address: Address.read
    })
  end
end

# Querying a person's name:
person = Person.read
person.address.country.name # => "Country Bar"

Terrifying, isn’t it?

Now consider this: the Address class could actually return an empty OpenStruct instead of a populated one, or worse, just a null object. You would get NoMethodError being raised on almost every step you take when navigating this object. Wow…

Now that we have power to #dig up this, we could transform everything into a hash and just dig it!

# No more OpenStructs!

class Country
  def self.read
    { name: "Country Bar" }
  end
end

class Address
  def self.read
    { country: Country.read }
  end
end

class Person < OpenStruct
  def self.read
    { address: Address.read }
  end
end

# Querying a person's name:
person = Person.read
person.dig(:address, :country, :name) # => "Country Bar"

This is way more readable and faster than very slow Ruby OpenStructs.

Is it fast enough?

What about #dig’s speed?

Let’s compare some #dig methods with old ways to access Hashes and Arrays values:

require 'benchmark/ips'

Benchmark.ips do |x|
  example_hash  = { a: { b: { c: 3 } } }
  example_array = [1, [2, [3]]]

  x.report("Hash#dig found")             { example_hash.dig(:a, :b, :c) }
  x.report("Hash#dig not found")         { example_hash.dig(:a, :b, :foo, :bar) }
  x.report("Hash navigation found")      { example_hash[:a][:b][:c] }
  x.report("Hash navigation not found")  { example_hash[:a][:b][:d] }

  x.report("Array#dig found")            { example_array.dig(1, 1, 0) }
  x.report("Array#dig not found")        { example_array.dig(1, 1, 1, 1) }
  x.report("Array navigation found")     { example_array[1][1][0] }
  x.report("Array navigation not found") { example_array[1][1][1] }

  x.compare!
end
Comparison:
Array navigation not found: 10120102.0 i/s
Array navigation found:     9955150.5 i/s - same-ish: difference falls within error
Array#dig found:            7998485.3 i/s - 1.27x  slower
Array#dig not found:        7855449.0 i/s - 1.29x  slower

Hash navigation not found:  7713907.2 i/s - 1.31x  slower
Hash navigation found:      7582034.4 i/s - 1.33x  slower
Hash#dig found:             6896451.4 i/s - 1.47x  slower
Hash#dig not found:         6483433.9 i/s - 1.56x  slower

Worst case scenario is a 1.5x slower method, which still operates above the 6 million operations per second. Yep, this is fast enough.

Conclusion

The new #dig method versatility is amazing, as it now provides Ruby programmers the ability of querying unknown objects with ease and safety. It’s also fast enough to avoid any worries around performance when using it.

One more great addition to Ruby’s incredible API :)