How to avoid NoMethodError for missing elements in nested hashes, without repeated nil checks?

0 votes
asked Dec 6, 2010 by kevin-sylvestre

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?

15 Answers

0 votes
answered Jan 6, 2010 by kyle-heironimus

If it's rails, use

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

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

0 votes
answered Jan 6, 2010 by thiago-silveira

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

name = params[:company][:owner][:name] rescue nil
0 votes
answered Jan 6, 2010 by mpd

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)

0 votes
answered Jan 6, 2010 by chuck

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.

0 votes
answered Jan 6, 2010 by phrogz

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.

0 votes
answered Jan 6, 2010 by andrew-grimm

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?

0 votes
answered Jan 7, 2010 by stephen-petschulat

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:

0 votes
answered Jan 7, 2010 by andrew-grimm

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
0 votes
answered Jan 7, 2010 by bill-dueber

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.

0 votes
answered Jan 1, 2011 by sawa

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']
Welcome to Q&A, where you can ask questions and receive answers from other members of the community.
Website Online Counter

...