ruby-on-railsrubyruby-hash

Could someone explain me what is the difference between Hash#dig vs Hash#fetch


I'm trying to obtain a nested value in a hash. I've tried using Hash#fetch and Hash#dig but I don't understand how they should be combined.

My hash is as follows.

response = {
   "results":[
      {
         "type":"product_group",
         "value":{
            "destination":"Rome"
         }
      },
      {
         "type":"product_group",
         "value":{
            "destination":"Paris"
         }
      },
      {
         "type":"product_group",
         "value":{
            "destination":"Madrid"
         }
      }
   ]
}

I've tried the following

response.dig(:results)[0].dig(:value).dig(:destination) #=> nil
response.dig(:results)[0].dig(:value).fetch('destination') #=> Rome

The desired return value is "Rome". The second expression works but I would like to know if it can be simplified.

I'm using Ruby v2.5 and Rails v5.2.1.1.


Solution

  • Hash#fetch is not relevant here. That's because fetch is the same as Hash#[] when, as here, fetch has only a single argument. So let's concentrate on dig.

    A family of three dig methods were introduced in Ruby v2.3: Hash#dig, Array#dig and OpenStruct#dig. An interesting thing about these methods is that they invoke each other (but that's not explained in the docs, not even in the examples). In your problem we can write:

    response.dig(:results, 0, :value, :destination)
      #=> "Rome" 
    

    response is a hash so Hash#dig evaluates response[:results]. If it's value is nil then the expression returns nil. For example,

    response.dig(:cat, 0, :value, :destination)
      #=> nil
    

    In fact, response[:results] is an array:

    arr = response[:results]
      #=> [{:type=>"product_group", :value=>{:destination=>"Rome"}},
      #    {:type=>"product_group", :value=>{:destination=>"Paris"}},
      #    {:type=>"product_group", :value=>{:destination=>"Madrid"}}]
    

    Hash#dig therefore invokes Array#dig on arr, obtaining the hash

    h = arr.dig(0)
      #=> {:type=>"product_group", :value=>{:destination=>"Rome"}} 
    

    Array#dig then invokes Hash#dig on h:

    g = h.dig(:value)
      #=> {:destination=>"Rome"}
    

    Lastly, g being a hash, Hash#dig invokes Hash#dig on g:

    g.dig(:destination)
      #=> "Rome"
    

    Caution needs to be exercised when using any of the dig's. Suppose

    arr = [[1,2], [3,[4,5]]]
    

    and we wish to pull out the object that is now occupied by 4. We could write either

    arr[1][1][0]
      #=> 4
    

    or

    arr.dig(1,1,0)
      #=> 4
    

    Now suppose arr were changed as follows:

    arr = [[1,2]]
    

    Then

    arr[1][1][0]
      #=> NoMethodError: undefined method `[]' for nil:NilClass
    

    and

    arr.dig(1,1,0)
      #=> nil
    

    Both are indicative of there being an error in our code, so raising an exception would be preferable to nil being returned, which may go unnoticed for some time.