27 July 2006
Using Ruby hashes as keyword arguments, with easy defaults
Similar to many Rails helpers/methods, a lot of the methods I write often use an optional hash of options, or sometimes just a hash only, to simulate keyword arguments (often using symbols).
The only downside to doing this is you lose out on easily setting default values using Ruby's default method argument values. You might use code something similar to the following to make up for this:
def some_method(opts={})
my_foo = opts[:foo] || 'mydefaultfoo'
end
However, as you have more and more keyword options, setting defaults in this way gets rather tedious. Fortunately, Ruby's Hash#merge comes to our rescue (almost) - it allows you to merge the contents of one hash with another. The only problem - any duplicate keys in the hash you are merging will overwrite your original hash values - when it comes to setting default values, we want this to work the other way around; we only want values in the defaults hash to be merged if they do not exist in the original hash. Again, Ruby comes to our rescue - Hash#merge takes a block as an argument and will pass any duplicate values that crop up into the block - we can use this block to decide which value to keep.
Using the simple monkey patch to the Hash class below, you will no longer have to set each default individually:
class Hash
def with_defaults(defaults)
self.merge(defaults) { |key, old, new| old.nil? ? new : old }
end
def with_defaults!(defaults)
self.merge!(defaults) { |key, old, new| old.nil? ? new : old }
end
end
Of course, sticking with Ruby naming conventions, with_defaults() will return a new hash whilst with_defaults!() will change the original hash directly. Now all you have to do is something like this:
def my_funky_method(opts={})
opts.with_defaults! {
:arg_one => 'foo',
:arg_two => 'bar',
:arg_three => 'baz'
}
end
Update: Dan Web just informed me of another way of doing this - instead of merging your defaults into your options, simply merge your options into your defaults et voila!
def my_method(opts={})
{:arg_one => 'foo', :arg_two => 'two'}.merge!(opts)
end
If there is one thing that is annoying about Ruby, it's that just when you think you've come up with a cool little code snippet, somebody inevitably comes up with an even easier way of doing the same thing.
That said, I do still think there is something nice about the with_defaults() method - it's slightly more intention-revealing. Weigh that up with the need to monkey-patch. Choose what works best for you.
Update 2: Josh Susser now informs me (see comments) that this functionality is built-right into Rails courtesy of ReverseMerge. I still think with_defaults sounds better but I guess that pretty much makes this post redundant!
Return to home page | Check out my tumblelog
9 Comments on this article
Return to home page | Check out my tumblelog
Commenting on this article is now closed
1. Comment by blank on 27 Jul 2006 at 15:07
Usually I do something similar
def my<em>funky</em>method(opts={}) defaults = { :arg1 => 'default', :arg2 => 'default' } opts = defaults.update opts ... end2. Comment by Luke Redpath on 27 Jul 2006 at 15:07
Thats actually the same as the bottom example, only more verbose (update is a synonym for merge).
3. Comment by josh susser on 27 Jul 2006 at 15:07
It gets even better. Rails’ ActiveSupport provides methods Hash#reversemerge and Hash#reversemerge! for doing Dan Webb’s suggestion in a single method. Over and over I find that someone in Rails has already thought of my clever idea first. How does it feel to have your entire blog post reduced to a single method in ActiveSupport? :-)
4. Comment by Luke Redpath on 27 Jul 2006 at 15:07
What can I say, you learn something new every day!
Though I still prefer the name with_defaults ;)
5. Comment by Rob Sanheim on 10 Aug 2006 at 04:08
Hey – just do a alias and you have your preferred name. Gotta love it.
6. Comment by Luke Redpath on 10 Aug 2006 at 08:08
True, but is it worth the effort of monkey patching and adding a file to your library?
Perhaps it might be worth writing a Rails plugin as a container for little addons like this.
7. Comment by Matt Buck on 24 May 2007 at 19:05
Couldn’t you just use a default argument?
def mymethod(options = {:limit => 5}) do_something options[:limit] end
8. Comment by mahesh on 04 Jul 2007 at 09:07
thank u
9. Comment by Pablo Castellazzi on 26 Sep 2007 at 03:09
This all are good solutions for a common problem, but you end up filtering arguments for every method you write. May be a general aproach will be more usefull in the long run.
Something like:
def options_filter(args, defaults = {}, &block) options = (args.last.is_a? Hash) ? args.pop : {} unknown = (options.keys – defaults.keys) raise(ArgumentError, “Block expected”) if not block_given? raise(ArgumentError, “Invalid options list: #{unknown.collect {|x| x.to_s}.join(”, “)}”) if not unknown.empty? block.call(defaults.merge(options), args) end
def my_method(*args) options_filter(args, { :a => 1, :b => 2 }) do |options, values| options.each_pair { |k, v| puts “Option: #{k} => #{v}” } values.each { |v| puts “Value: #{v}” } end end
my_method :bla, :ble, :bli, :b => 4