Do, or do not. There is no #try.

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? or rescue.
  • It's not just for hashes: this will work with any container that supports #fetch and #[].

I hope someone finds this useful.

EDIT: The discussion of #try and its design ramifications is continued in the next article.