Fetch VS. [] When Working with Hashes

fetch vs. [] when working with hashes?

By default, using #[] will retrieve the hash value if it exists, and return nil if it doesn't exist *.

Using #fetch gives you a few options (see the docs on #fetch):

  • fetch(key_name): get the value if the key exists, raise a KeyError if it doesn't
  • fetch(key_name, default_value): get the value if the key exists, return default_value otherwise
  • fetch(key_name) { |key| "default" }: get the value if the key exists, otherwise run the supplied block and return the value.

Each one should be used as the situation requires, but #fetch is very feature-rich and can handle many cases depending on how it's used. For that reason I tend to prefer it over accessing keys with #[].

* As Marc-André Lafortune said, accessing a key with #[] will call #default_proc if it exists, or else return #default, which defaults to nil. See the doc entry for ::new for more information.

Benefits of using `Hash#fetch` over `Hash#[]`

Three main uses:

  1. When the value is mandatory, i.e. there is no default:

    options.fetch(:repeat).times{...}

    You get a nice error message too:

    key not found: :repeat
  2. When the value can be nil or false and the default is something else:

    if (doit = options.fetch(:repeat, 1))
    doit.times{...}
    else
    # options[:repeat] is set to nil or false, do something else maybe
    end
  3. When you don't want to use the default/default_proc of a hash:

    options = Hash.new(42)
    options[:foo] || :default # => 42
    options.fetch(:foo, :default) # => :default

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

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.

How do I use the fetch method for nested hash?

EDIT: there is a built-in way now, see this answer.


There is no built-in method that I know of. I have this in my current project

class Hash
def fetch_path(*parts)
parts.reduce(self) do |memo, key|
memo[key.to_s] if memo
end
end
end

# usage
hash.fetch_path('name', 'Mike', 'age')

You can easily modify it to use #fetch instead of #[] (if you so wish).

`try` method when trying to fetch hash value

If you need to allow for object.content.nil?, then you'd use try. If you want to allow for a missing key then you don't want fetch (as Priti notes), you want the normal [] method. Combining the two yields:

object.content.try(:[], 'en')

Observe:

> h = { :a => :b }
=> {:a=>:b}
> h.try(:[], :a)
=> :b
> h.try(:[], :c)
=> nil
> h = nil
=> nil
> h.try(:[], :a)
=> nil

You could also use object.content.try(:fetch, 'en', nil) if :[] looks like it is mocking you.

How to get a default value with hashes in ruby

You just need to give fetch a default it can handle. It doesn't know what to do with january as you haven't declared any variable with that name. If you want to set the default value to the string "january", then you just need to quote it like this:

cohort.fetch(:cohort, "january") 

There are some decent examples in the documentation for fetch.

Also, cohort isn't a Hash, it's a String since gets.chomp returns a String. fetch is for "fetching" values from a Hash. The way you're using it should be throwing an error similar to: undefined method 'fetch' for "whatever text you entered":String.

Finally, since you're using it in a conditional, the result of your call to fetch is being evaluated for its truthiness. If you're setting a default, it will always be evaluated as true.

If you just want to set a default for cohort if it's empty, you can just do something like this:

cohort = gets.chomp
cohort = "january" if cohort.empty?
while !name.empty? && !hobbies.empty? && !country.empty?
students << {
name: name,
hobbies: hobbies,
country: country,
cohort: cohort
}
... # do more stuff

Hope that's helpful.

hash[key] is to hash.fetch(key) as @ivar is to what?

There is no built in method that provides this functionality in respect to instance variables, however;

Since Hash#[] is analogous to @ivar (or instance_variable_get(:@ivar)) in your example then Hash#fetch would be analogous to

def instance_variable_fetch(sym)
raise(NameError, "instance variable not found: #{sym}") unless instance_variable_defined?(sym)
instance_variable_get(sym)
end

Example:

@var = 42
instance_variable_fetch(:@var)
#=> 42
@ivar = nil
instance_variable_fetch(:@ivar)
#=> nil
instance_variable_fetch(:@other_var)
#=> NameError: instance variable not found: @other_var


Related Topics



Leave a reply



Submit