Equivalent of .try() for a hash to avoid "undefined method" errors on nil?
Solution 1
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]
.
Solution 2
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
andHash#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.
Solution 3
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)".
Solution 4
The proper use of try with a hash is @sesion.try(:[], :comments)
.
@session.try(:[], :comments).try(:[], commend.id).try(:[], 'temp_value')
Solution 5
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'
Related videos on Youtube
sscirrus
Updated on September 14, 2020Comments
-
sscirrus almost 4 years
In Rails we can do the following in case a value doesn't exist to avoid an error:
@myvar = @comment.try(:body)
What is the equivalent when I'm digging deep into a hash and don't want to get an error?
@myvar = session[:comments][@comment.id]["temp_value"] # [:comments] may or may not exist here
In the above case,
session[:comments]try[@comment.id]
doesn't work. What would?-
oligan about 13 yearsRelated question: stackoverflow.com/questions/4371716/…
-
user513951 over 8 yearsRuby 2.3 introduced
Hash#dig
that makestry
unnecessary here. @baxang has the best answer now. -
Markus Andreas over 4 yearsDig does not make try unnexessary, because it sill fails on other objects than hash. For exaple nil. But using dig in combination with the save operator does => session&.dig(:comments, @comment.id, "temp_value")
-
-
sscirrus about 13 yearshow about if I don't know if either
[:comments]
or[@comment.id]
exist? -
bor1s about 13 yearsin this case I think it would be better to create nested IF statements to check every parameter in session
-
oligan about 13 years-1 Why can't it be nested?
try
applies to anyObject
, andnil
is anObject
, so I suspect the following would work:nil.try(:do).try(:do_not).try(:there_is_a_try)
. -
oligan about 13 years@sscirrus: You could do
session[:comments][@comment.id]["temp_value"] if (session[:comments] and session[:comments][@comment.id])
-
Pablo Castellazzi about 13 yearsThe "cant be nested" is wrong. But for your particular case my appreciation was correct. what you need to do is use try with :[], for use it with the key directly you need to use fetch.
-
sscirrus about 13 yearsThis is fascinating - thanks Max! Are there any disadvantages to this you know of? Does anyone else have a perspective on this?
-
sscirrus about 13 years@AndrewGrimm - yeah, I figured that would work but I was hoping for something more concise (I would have a few similar expressions in one place, and it looks very code-heavy). I like your actual answer. :)
-
Arsen7 about 13 yearsIt will hide your problems with unexpected nils in other parts of your code. I would consider this method dangerous.
-
riffraff almost 13 yearsactually, since
nil
is not aHash
you can probably simplify tofields.inject(self) {|acc,e| acc[e] if acc.is_a?(Hash)}
But I have a feeling#respond_to
would be better. -
Arsen7 almost 13 years@riffraff: You are perfectly right about that
acc & acc.is_a?()
- consider that a mistake ;-). Butrespond_to
would not work, because String and a lot of other objects also respond to:[]
, but the result of this method is not what is wanted here. -
svoop over 12 yearsSince
:[]
looks a little weird withintry
, you could also write this assession[:comments].try(:fetch, @comment.id)
. -
rigyt about 12 yearsfetch throws an error if the key is not found, unless you pass a default. So you would need to write: session[:comments].try(:fetch, @comment.id, nil)
-
Jeff Dickey over 11 yearsbecause that's not what
.try
does. -
Augustin Riedinger about 9 yearsThe point of using
object.try
is thatobject
can benil
. Whereas in your casenil.get_deep
will raise an exception. Your solution doesn't answer the question then. -
Arsen7 about 9 yearsThe question says "when I'm digging deep into a hash", and I did not assume that a
session
could benil
, but if it could, then it would be perfectly OK to callsession.try(:get_deep, :comments, @comment.id, "temp_value")
-
user513951 over 8 years
try
is no longer necessary as of Ruby 2.3. @baxang has the best answer, below. -
emptywalls about 8 yearsLooks like session doesn't implement dig: undefined method `dig' for #<ActionDispatch::Request::Session:0x007ffc6cafa698>
-
JBlake about 8 yearsCan't use dig on the session hash
-
kamal almost 8 yearswhat if nil? I want to return [], instead of nil.
-
JustGage over 7 yearsbreaks if the hash is
nil
for those who where wondering -
epylinkn over 7 yearsNote that this doesn't actually work with a Rails session as ActionDispatch::Request::Session doesn't implement #dig
-
Renan over 7 yearsalso breaks if
params[:country]
is not ahash
ornil
(e.g. astring
) -
thisismydesign almost 7 years&. does not break on
nil
(unlike.dig
by itself). Therefore a safe implementation is:params&.dig(:country, :state, :city)
-
hongchangfirst almost 5 yearsSorry, I'm new to ruby, what's :[] here?
-
svelandiag about 3 yearsBest answer, working okay with regular hashes