183

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?

user513951
  • 12,445
  • 7
  • 65
  • 82
sscirrus
  • 55,407
  • 41
  • 135
  • 228
  • 2
    Related question: http://stackoverflow.com/questions/4371716/looking-for-a-good-way-to-avoid-hash-conditionals-in-ruby – Andrew Grimm Jun 03 '11 at 09:22
  • 5
    Ruby 2.3 introduced `Hash#dig` that makes `try` unnecessary here. @baxang has the best answer now. – user513951 Jan 06 '16 at 01:41
  • Dig 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") – Markus Andreas Dec 13 '19 at 10:23

12 Answers12

292

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].

mkenyon
  • 470
  • 1
  • 5
  • 15
Andrew Grimm
  • 78,473
  • 57
  • 200
  • 338
74

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.

baxang
  • 3,627
  • 1
  • 29
  • 27
25

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)".

Community
  • 1
  • 1
Arsen7
  • 12,522
  • 2
  • 43
  • 60
  • 1
    actually, since `nil` is not a `Hash` you can probably simplify to `fields.inject(self) {|acc,e| acc[e] if acc.is_a?(Hash)}` But I have a feeling `#respond_to`would be better. – riffraff Sep 01 '11 at 15:23
  • 1
    @riffraff: You are perfectly right about that `acc & acc.is_a?()` - consider that a mistake ;-). But `respond_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. – Arsen7 Sep 02 '11 at 08:03
  • The point of using `object.try` is that `object` can be `nil`. Whereas in your case `nil.get_deep` will raise an exception. Your solution doesn't answer the question then. – Augustin Riedinger Jun 02 '15 at 15:56
  • The question says "when I'm digging deep into a hash", and I did not assume that a `session` could be `nil`, but if it could, then it would be perfectly OK to call `session.try(:get_deep, :comments, @comment.id, "temp_value")` – Arsen7 Jun 19 '15 at 14:46
17

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'
Community
  • 1
  • 1
Benjamin Dobell
  • 4,042
  • 1
  • 32
  • 44
17

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

@session.try(:[], :comments).try(:[], commend.id).try(:[], 'temp_value')
Pablo Castellazzi
  • 4,164
  • 23
  • 20
  • -1 Why can't it be nested? `try` applies to any `Object`, and `nil` is an `Object`, so I suspect the following would work: `nil.try(:do).try(:do_not).try(:there_is_a_try)`. – Andrew Grimm Jun 03 '11 at 09:17
  • 2
    The "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. – Pablo Castellazzi Jun 03 '11 at 09:29
14

say you want to find params[:user][:email] but it's not sure whether user is there in params or not. Then-

you can try:

params[:user].try(:[], :email)

It will return either nil(if user is not there or email is not there in user) or otherwise the value of email in user.

Rajesh Paul
  • 6,793
  • 6
  • 40
  • 57
14

As of Ruby 2.3 this gets a little easier. Instead of having to nest try statements or define your own method you can now use Hash#dig (documentation).

h = { foo: {bar: {baz: 1}}}

h.dig(:foo, :bar, :baz)           #=> 1
h.dig(:foo, :zot)                 #=> nil

Or in the example above:

session.dig(:comments, @comment.id, "temp_value")

This has the added benefit of being more like try than some of the examples above. If any of the arguments lead to the hash returning nil then it will respond nil.

Steve Smith
  • 5,146
  • 1
  • 30
  • 31
14
@myvar = session.fetch(:comments, {}).fetch(@comment.id, {})["temp_value"]

From Ruby 2.0, you can do:

@myvar = session[:comments].to_h[@comment.id].to_h["temp_value"]

From Ruby 2.3, you can do:

@myvar = session.dig(:comments, @comment.id, "temp_value")
sawa
  • 165,429
  • 45
  • 277
  • 381
8

Another approach:

@myvar = session[:comments][@comment.id]["temp_value"] rescue nil

This might also be consider a bit dangerous because it can hide too much, personally I like it.

If you want more control, you may consider something like:

def handle # just an example name, use what speaks to you
    raise $! unless $!.kind_of? NoMethodError # Do whatever checks or 
                                              # reporting you want
end
# then you may use
@myvar = session[:comments][@comment.id]["temp_value"] rescue handle
Nicolas Goy
  • 1,294
  • 9
  • 21
1

Andrew's answer didn't work for me when I tried this again recently. Maybe something has changed?

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

The '[]' is in quotes instead of a symbol :[]

claptimes
  • 1,615
  • 15
  • 20
1

When you do this:

myhash[:one][:two][:three]

You're just chaining a bunch of calls to a "[]" method, an the error occurs if myhash[:one] returns nil, because nil doesn't have a [] method. So, one simple and rather hacky way is to add a [] method to Niclass, which returns nil: i would set this up in a rails app as follows:

Add the method:

#in lib/ruby_extensions.rb
class NilClass
  def [](*args)
    nil
  end
end

Require the file:

#in config/initializers/app_environment.rb
require 'ruby_extensions'

Now you can call nested hashes without fear: i'm demonstrating in the console here:

>> hash = {:foo => "bar"}
=> {:foo=>"bar"}
>> hash[:foo]
=> "bar"
>> hash[:doo]
=> nil
>> hash[:doo][:too]
=> nil
Max Williams
  • 32,435
  • 31
  • 130
  • 197
  • This is fascinating - thanks Max! Are there any disadvantages to this you know of? Does anyone else have a perspective on this? – sscirrus Jun 03 '11 at 19:23
  • 17
    It will hide your problems with unexpected nils in other parts of your code. I would consider this method dangerous. – Arsen7 Jun 06 '11 at 09:17
-1

Try to use

@myvar = session[:comments][@comment.id]["temp_value"] if session[:comments]
bor1s
  • 4,081
  • 18
  • 25
  • how about if I don't know if either `[:comments]` or `[@comment.id]` exist? – sscirrus Jun 03 '11 at 08:52
  • in this case I think it would be better to create nested IF statements to check every parameter in session – bor1s Jun 03 '11 at 08:58
  • 1
    @sscirrus: You could do `session[:comments][@comment.id]["temp_value"] if (session[:comments] and session[:comments][@comment.id])` – Andrew Grimm Jun 03 '11 at 09:20
  • @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. :) – sscirrus Jun 03 '11 at 21:10