32

I'm looking for a good way to avoid checking for nil at each level in deeply nested hashes. For example:

name = params[:company][:owner][:name] if params[:company] && params[:company][:owner] && params[:company][:owner][:name]

This requires three checks, and makes for very ugly code. Any way to get around this?

Kevin Sylvestre
  • 37,288
  • 33
  • 152
  • 232
  • 1
    In groovy you would use the `?` operator. Actually i'm interested by an equivalent operator. You can still extend the hash class and add the operator. – Pasta Dec 06 '10 at 22:54
  • @Pasta [Io](http://iolanguage.com) has a similar operator, but Ruby does not. – Phrogz Dec 06 '10 at 23:19

16 Answers16

37

Ruby 2.3.0 introduced a method called dig on both Hash and Array.

name = params.dig(:company, :owner, :name)

It returns nil if the key is missing at any level.

If you are using a version of Ruby older than 2.3, you can install a gem such as ruby_dig or hash_dig_and_collect, or implement the functionality yourself:

module RubyDig
  def dig(key, *rest)
    if value = (self[key] rescue nil)
      if rest.empty?
        value
      elsif value.respond_to?(:dig)
        value.dig(*rest)
      end
    end
  end
end

if RUBY_VERSION < '2.3'
  Array.send(:include, RubyDig)
  Hash.send(:include, RubyDig)
end
user513951
  • 12,445
  • 7
  • 65
  • 82
  • `params.dig` will fail if `params` is `nil`. Consider using the safe navigation operator instead or combined with `.dig` as: `params&.dig(:company, :owner, :name)` or `params&.company&.owner&.name`. – thisismydesign Sep 11 '17 at 15:24
  • 2
    The syntax of the safe navigator operator on hashes in my previous comment is incorrect. The correct syntax is: `params&.[](:company)&.[](:owner)&.[](:name)`. – thisismydesign Sep 18 '17 at 15:24
11

The best compromise between functionality and clarity IMO is Raganwald's andand. With that, you would do:

params[:company].andand[:owner].andand[:name]

It's similar to try, but reads a lot better in this case since you're still sending messages like normal, but with a delimiter between that calls attention to the fact that you're treating nils specially.

Chuck
  • 234,037
  • 30
  • 302
  • 389
  • 1
    +1: I was going to recommend maybe's Ick (also from Raganwald), which is the same idea, you can also include a link in the answer: http://ick.rubyforge.org/ – tokland Dec 06 '10 at 23:23
  • 3
    IMO `andand` is syntactically disgusting – mpd Dec 07 '10 at 02:43
  • @mpd: Why? In concept or you just don't like that particular word? – Chuck Dec 07 '10 at 05:21
  • @chuck I like the concept, but it seems very un-elegant. It also is confusing if you don't know what it is being used for, I mean `andand` just doesn't make sense(I understand the reference to `&&`). I don't think it properly conveys it's meaning with it's name. That being said, I like it better than `try` – mpd Dec 07 '10 at 05:38
7

I don't know if that's what you want, but maybe you could do this?

name = params[:company][:owner][:name] rescue nil
Thiago Silveira
  • 5,033
  • 4
  • 26
  • 29
  • 11
    sorry to say that but indiscriminate rescues are evil, you can mask so many non-related errors... – tokland Dec 06 '10 at 23:25
  • Yes, EEEEeeevil with a capital "E". – the Tin Man Dec 07 '10 at 00:12
  • 4
    Since the only thing happening here is hash lookups with symbols, this seems to me like a very discriminate rescue, and exactly what I do. – glenn mcdonald Dec 07 '10 at 00:36
  • You may select the exceptions you want to catch, like so: http://stackoverflow.com/questions/6224875/equivalent-of-try-for-a-hash/17686902#17686902 – Nicolas Goy Sep 24 '13 at 15:58
  • @glennmcdonald This code by no way ensures that `params` is a hash. `rescue nil` is still a no-go. There're better, effortless solutions posted here. There's no reason to take the risk and try to be smart about this. – thisismydesign Nov 28 '18 at 23:52
4

Update: This answer is way out-of-date. Use dig as the current accepted answer suggests.

If it's Rails, use

params.try(:[], :company).try(:[], :owner).try(:[], :name)

Oh wait, that's even uglier. ;-)

Kyle Heironimus
  • 7,741
  • 7
  • 39
  • 51
4

You may want to look into one of the ways to add auto-vivification to ruby hashes. There are a number of approaches mentioned in the following stackoverflow threads:

Community
  • 1
  • 1
Stephen Petschulat
  • 1,159
  • 2
  • 14
  • 23
  • Thanks Stephen. I'd never heard of *auto-vivification* before but it would be perfect if I was defining the hash. I appreciate the answer! – Kevin Sylvestre Dec 07 '10 at 00:44
  • how about editing your answer and making the links more apparent. It's hard to tell what the last two point to. – the Tin Man Dec 07 '10 at 03:35
4

Equivalent to the second solution that user mpd suggested, only more idiomatic Ruby:

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

hash = {a: {b: {c: 3, d: 4}}}

p hash.deep_fetch :a, :b, :c
#=> 3
p hash.deep_fetch :a, :b
#=> {:c=>3, :d=>4}
p hash.deep_fetch :a, :b, :e
#=> nil
p hash.deep_fetch :a, :b, :e, :f
#=> nil
Mladen Jablanović
  • 43,461
  • 10
  • 90
  • 113
  • 1
    A slightly improved method is here: http://stackoverflow.com/questions/6224875/equivalent-of-try-for-a-hash#6225321 – Josh Jul 06 '13 at 00:52
  • And a slightly more improved than 'slightly improved' method is here: http://stackoverflow.com/a/27498050/199685 – Benjamin Dobell Dec 16 '14 at 05:42
3

If you wanna get into monkeypatching you could do something like this

class NilClass
  def [](anything)
    nil
  end
end

Then a call to params[:company][:owner][:name] will yield nil if at any point one of the nested hashes is nil.

EDIT: If you want a safer route that also provides clean code you could do something like

class Hash
  def chain(*args)
    x = 0
    current = self[args[x]]
    while current && x < args.size - 1
      x += 1
      current = current[args[x]]
    end
    current
  end
end

The code would look like this: params.chain(:company, :owner, :name)

mpd
  • 2,223
  • 1
  • 19
  • 23
  • 2
    I like this solution because it's clever and leads to really clean code. But boy it sure feels dangerous to me. You'll never know if an array is actually nil throughout your entire app. – Matt Greer Dec 06 '10 at 23:18
  • 1
    Yeah, that is a big downside to this approach.However, there are some other tricks that can be done within the method definition to warn you when this occurs. It's really just an idea, one that can be tailored to the programmers needs. – mpd Dec 06 '10 at 23:21
  • 3
    This works, but is kind of dangerous because you're monkey-patching a very fundamental part of Ruby to work in a completely different way. – Chuck Dec 06 '10 at 23:24
  • Yeah, I'm still very fearful of monkey-patches! – Kevin Sylvestre Dec 07 '10 at 00:31
2

I would write this as:

name = params[:company] && params[:company][:owner] && params[:company][:owner][:name]

It's not as clean as the ? operator in Io, but Ruby doesn't have that. The answer by @ThiagoSilveira is also good, though it will be slower.

Phrogz
  • 296,393
  • 112
  • 651
  • 745
1

(Even though it's a really old question maybe this answer will be useful for some stackoverflow people like me that did not think of the "begin rescue" control structure expression.)

I would do it with a try catch statement (begin rescue in ruby language):

begin
    name = params[:company][:owner][:name]
rescue
    #if it raises errors maybe:
    name = 'John Doe'
end
Trond Hatlen
  • 571
  • 4
  • 10
  • And what if I typo name = parms[:company][:owner][:name]? The code will happily go along with 'John Doe', and I may never notice. – Edward Anderson Sep 18 '13 at 23:27
  • That's true it should've been nil in the rescue case since thats what the question was using. I now see that Thiago Silveira's answer was exactly what i was thinking but more elegant. – Trond Hatlen Sep 19 '13 at 07:22
1

Are you able to avoid using a multi-dimensional hash, and use

params[[:company, :owner, :name]]

or

params[[:company, :owner, :name]] if params.has_key?([:company, :owner, :name])

instead?

Andrew Grimm
  • 78,473
  • 57
  • 200
  • 338
  • Thanks for the response Andrew. I'm not able to avoid the multi-dimensional hash (unfortunately) as the hash is being passed from an external library. – Kevin Sylvestre Dec 07 '10 at 00:24
1

Write the ugliness once, then hide it

def check_all_present(hash, keys)
  current_hash = hash
  keys.each do |key|
    return false unless current_hash[key]
    current_hash = current_hash[key]
  end
  true
end
Andrew Grimm
  • 78,473
  • 57
  • 200
  • 338
  • I think this might be better and more useful to the OP (and common) needs if the return value was the last item in the chain. – Phrogz Dec 07 '10 at 04:31
1

Do:

params.fetch('company', {}).fetch('owner', {})['name']

Also at each step, you can use an appropriate method built in NilClass to escape from nil, if it were array, string, or numeric. Just add to_hash to the inventory of this list and use it.

class NilClass; def to_hash; {} end end
params['company'].to_hash['owner'].to_hash['name']
Andrew Grimm
  • 78,473
  • 57
  • 200
  • 338
sawa
  • 165,429
  • 45
  • 277
  • 381
0

You don't need access to the original hash definition -- you can override the [] method on the fly after you get it using h.instance_eval, e.g.

h = {1 => 'one'}
h.instance_eval %q{
  alias :brackets :[]
  def [] key
    if self.has_key? key
      return self.brackets(key)
    else
      h = Hash.new
      h.default = {}
      return h
    end
  end
}

But that's not going to help you with the code you have, because you're relying on an unfound value to return a false value (e.g., nil) and if you do any of the "normal" auto-vivification stuff linked to above you're going to end up with an empty hash for unfound values, which evaluates as "true".

You could do something like this -- it only checks for defined values and returns them. You can't set them this way, because we've got no way of knowing if the call is on the LHS of an assignment.

module AVHash
  def deep(*args)
    first = args.shift
    if args.size == 0
      return self[first]
    else
      if self.has_key? first and self[first].is_a? Hash
        self[first].send(:extend, AVHash)
        return self[first].deep(*args)
      else
        return nil
      end
    end
  end
end      

h = {1=>2, 3=>{4=>5, 6=>{7=>8}}}
h.send(:extend, AVHash)
h.deep(0) #=> nil
h.deep(1) #=> 2
h.deep(3) #=> {4=>5, 6=>{7=>8}}
h.deep(3,4) #=> 5
h.deep(3,10) #=> nil
h.deep(3,6,7) #=> 8

Again, though, you can only check values with it -- not assign them. So it's not real auto-vivification as we all know and love it in Perl.

Bill Dueber
  • 2,706
  • 17
  • 16
0

Dangerous but works:

class Object
        def h_try(key)
            self[key] if self.respond_to?('[]')
        end    
end

We can new do

   user = { 
     :first_name => 'My First Name', 
     :last_name => 'my Last Name', 
     :details => { 
        :age => 3, 
        :birthday => 'June 1, 2017' 
      } 
   }

   user.h_try(:first_name) # 'My First Name'
   user.h_try(:something) # nil
   user.h_try(:details).h_try(:age) # 3
   user.h_try(:details).h_try(:nothing).h_try(:doesnt_exist) #nil

The "h_try" chain follows similar style to a "try" chain.

arashb31
  • 473
  • 6
  • 12
0

TLDR; params&.dig(:company, :owner, :name)

As of Ruby 2.3.0:

You can also use &. called the "safe navigation operator" as: params&.[](:company)&.[](:owner)&.[](:name). This one is perfectly safe.

Using dig on params is not actually safe as params.dig will fail if params is nil.

However you may combine the two as: params&.dig(:company, :owner, :name).

So either of the following is safe to use:

params&.[](:company)&.[](:owner)&.[](:name)

params&.dig(:company, :owner, :name)

thisismydesign
  • 21,553
  • 9
  • 123
  • 126
0

Just to offer a one-up on dig, try the KeyDial gem which I wrote. This is essentially a wrapper for dig but with the important difference that it will never hit you with an error.

dig will still spit out an error if an object in the chain is of some type that can't itself be diged.

hash = {a: {b: {c: true}, d: 5}}

hash.dig(:a, :d, :c) #=> TypeError: Integer does not have #dig method

In this situation dig does not help you, and you need to go back not only to hash[:a][:d].nil? && but also hash[:a][:d].is_a?(Hash) checks. KeyDial lets you do this without such checks or errors:

hash.call(:a, :d, :c) #=> nil
hash.call(:a, :b, :c) #=> true
Convincible
  • 314
  • 2
  • 10