Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Equivalent of .try() for a hash to avoid "undefined method" errors on nil? [duplicate]

You forgot to put a . before the try:

@myvar = session[:comments].try(:[], @comment.id)

since [] is the name of the method when you do [@comment.id].


The announcement of Ruby 2.3.0-preview1 includes an introduction of Safe navigation operator.

A safe navigation operator, which already exists in C#, Groovy, and Swift, is introduced to ease nil handling as obj&.foo. Array#dig and Hash#dig are also added.

This means as of 2.3 below code

account.try(:owner).try(:address)

can be rewritten to

account&.owner&.address

However, one should be careful that & is not a drop in replacement of #try. Take a look at this example:

> params = nil
nil
> params&.country
nil
> params = OpenStruct.new(country: "Australia")
#<OpenStruct country="Australia">
> params&.country
"Australia"
> params&.country&.name
NoMethodError: undefined method `name' for "Australia":String
from (pry):38:in `<main>'
> params.try(:country).try(:name)
nil

It is also including a similar sort of way: Array#dig and Hash#dig. So now this

city = params.fetch(:[], :country).try(:[], :state).try(:[], :city)

can be rewritten to

city = params.dig(:country, :state, :city)

Again, #dig is not replicating #try's behaviour. So be careful with returning values. If params[:country] returns, for example, an Integer, TypeError: Integer does not have #dig method will be raised.


The most beautiful solution is an old answer by Mladen Jablanović, as it lets you to dig in the hash deeper than you could with using direct .try() calls, if you want the code still look nice:

class Hash
  def get_deep(*fields)
    fields.inject(self) {|acc,e| acc[e] if acc}
  end
end

You should be careful with various objects (especially params), because Strings and Arrays also respond to :[], but the returned value may not be what you want, and Array raises exception for Strings or Symbols used as indexes.

That is the reason why in the suggested form of this method (below) the (usually ugly) test for .is_a?(Hash) is used instead of (usually better) .respond_to?(:[]):

class Hash
  def get_deep(*fields)
    fields.inject(self) {|acc,e| acc[e] if acc.is_a?(Hash)}
  end
end

a_hash = {:one => {:two => {:three => "asd"}, :arr => [1,2,3]}}

puts a_hash.get_deep(:one, :two               ).inspect # => {:three=>"asd"}
puts a_hash.get_deep(:one, :two, :three       ).inspect # => "asd"
puts a_hash.get_deep(:one, :two, :three, :four).inspect # => nil
puts a_hash.get_deep(:one, :arr            ).inspect    # => [1,2,3]
puts a_hash.get_deep(:one, :arr, :too_deep ).inspect    # => nil

The last example would raise an exception: "Symbol as array index (TypeError)" if it was not guarded by this ugly "is_a?(Hash)".


The proper use of try with a hash is @sesion.try(:[], :comments).

@session.try(:[], :comments).try(:[], commend.id).try(:[], 'temp_value')

Update: As of Ruby 2.3 use #dig

Most objects that respond to [] expect an Integer argument, with Hash being an exception that will accept any object (such as strings or symbols).

The following is a slightly more robust version of Arsen7's answer that supports nested Array, Hash, as well as any other objects that expect an Integer passed to [].

It's not fool proof, as someone may have created an object that implements [] and does not accept an Integer argument. However, this solution works great in the common case e.g. pulling nested values from JSON (which has both Hash and Array):

class Hash
  def get_deep(*fields)
    fields.inject(self) { |acc, e| acc[e] if acc.is_a?(Hash) || (e.is_a?(Integer) && acc.respond_to?(:[])) }
  end
end

It can be used the same as Arsen7's solution but also supports arrays e.g.

json = { 'users' => [ { 'name' => { 'first_name' => 'Frank'} }, { 'name' => { 'first_name' => 'Bob' } } ] }

json.get_deep 'users', 1, 'name', 'first_name' # Pulls out 'Bob'