One of the ways you know you are working in a good language is that it makes bad habits ugly. To wit:

# params[:foo] might be missing value = params[:foo].try(:[], :bar)

This is not pretty. It is, as my dad might say, “sick and ugly and it wants to die”.

(Yes, your dad would say that. KAG)

It is also, not coincidentally, completely unnecessary. Ruby is a language in which ugly things are more often than not superfluous.

Before I get to rewriting it, let me talk a little about the underlying problem here. The problem is uncertainty. And #try is not the answer. In fact, I’ll come right out and say it: #try is a code smell. It’s a good thing it’s just in ActiveSupport and not in Ruby proper. #try is a thin layer of faded 70s wallpaper pasted over a glaring violation of the “Tell, Don’t Ask” principle.

There is almost never a good excuse to use #try . The example above would be better written with fetch:

# provide a default value (an empty Hash) for params[:foo] value = params.fetch(:foo){{}}[:bar]

In some cases, where you have control over the original Hash, it can make more sense to give the Hash a default value other than nil:

params = Hash.new { {} } # ... value = params[:foo][:bar]

In other cases, when you have a really deeply nested structure, a Null Object might be more appropriate:

# See Null Objects article for implementation of Maybe() value = Maybe(params)[:foo][:bar][:baz][:buz]

But maybe a Null Object is a bigger gun than you want to haul out for this. I got

to thinking about this problem today, and here’s what I came up with, with some help from Larry Marburger:

h = { :foo => { :bar => [:x, :y, :z], :baz => Hash.new{ "missing value in :bar" } } } h.extend(DeepFetch) h[:foo] # => {:baz=>{}, :bar=>[:x, :y, :z]} h[:foo, :bar] # => [:x, :y, :z] h[:foo, :bar, 1] # => :y h[:foo, :bar, 5] # => nil h[:foo, :baz] # => {} h[:foo, :baz, :fuz] # => "missing value in :bar" h.deep_fetch(:foo, :bar, 0) # => :x h.deep_fetch(:buz) { :default_value } # => :default_value h.deep_fetch(:foo, :bar, 5) { :default_value } # => :default_value

Here’s the implementation of DeepFetch (also available as a Gist):

module DeepFetch def deep_fetch(*keys, &fetch_default) throw_fetch_default = fetch_default && lambda {|key, coll| args = [key, coll] # only provide extra block args if requested args = args.slice(0, fetch_default.arity) if fetch_default.arity >= 0 # If we need the default, we need to stop processing the loop immediately throw :df_value, fetch_default.call(*args) } catch(:df_value){ keys.inject(self){|value,key| block = throw_fetch_default && lambda{|*args| # sneak the current collection in as an extra block arg args < < value throw_fetch_default.call(*args) } value.fetch(key, &block) } } end # Overload [] to work with multiple keys def [](*keys) case keys.size when 1 then super else deep_fetch(*keys){|key, coll| coll[key]} end end end

Notable about this implementation:

There is no mucking about with #respond_to? or rescue .

or . It’s not just for hashes: this will work with any container that supports #fetch and #[] .

I hope someone finds this useful.