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”.
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?orrescue. - It's not just for hashes: this will work with any container that supports
#fetchand#[].
I hope someone finds this useful.
EDIT: The discussion of #try and its design ramifications is continued in the next article.





