Reading SBPP has got me thinking about keyword parameters. Ruby doesn’t have keyword arguments, but it fakes them pretty well:

def explain(options={})
  "the #{options[:the]} says #{options[:says]}"
end

explain the: "pig", says: "oink"    # => "the pig says oink"
explain the: "frog", says: "ribbit" # => "the frog says ribbit"

Which is fine, but it isn’t as declarative (and therefore not as self-documenting) as proper keyword arguments.

Also, when using keywords to construct English-like DSLs, as we are above, we often would like to assign different names to the parameters which are passed by keyword.

def explain2(options={})
  animal = options[:the]
  sound  = options[:says]
  "the #{animal} says #{sound}"
end

And then there’s defaulting for missing paramters…

def explain3(options={})
  animal = options.fetch(:the) { "cow" }
  sound  = options.fetch(:says){ "moo" }
  "the #{animal} says #{sound}"
end

explain3 # => "the cow says moo"

Of course, it might be nice to offer a positional-argument version as well.

def explain4(*args)
  options = args.last.is_a?(Hash) ? args.pop : {}
  animal  = args[0] || options.fetch(:the) { "cow" }
  sound   = args[1] || options.fetch(:says){ "moo" }
  "the #{animal} says #{sound}"
end

explain4 "horse", "neigh"        # => "the horse says neigh"
explain4 "duck", says: "quack"   # => "the duck says quack"
explain4 the: "donkey", :says => "hee-haw" # => "the donkey says hee-haw"

Once we’ve written all this parameter-munging machinery, we then repeat it in the method’s documentation. (Assuming we document it at all). This seems a bit un-DRY.

Let’s see if we can improve on the situation.

$ gem install keyword_params
Fetching: keyword_params-0.0.1.gem (100%)
Successfully installed keyword_params-0.0.1
1 gem installed
require 'keyword_params'

class BarnYard
  extend KeywordParams

  keyword(:the)  { "cow" }
  keyword(:says) { "moo" }
  def explain(animal, sound)
    "the #{animal} says #{sound}"
  end
end

b = BarnYard.new

b.explain "horse", "neigh"                  # => "the horse says neigh"
b.explain "duck", says: "quack"             # => "the duck says quack"
b.explain the: "donkey", says: "hee-haw"    # => "the donkey says hee-haw"
b.explain the: "cat"                        # => "the cat says moo"
b.explain                                   # => "the cow says moo"

This is just an alpha-level library I banged out during RubyConf 2011, so I’m sure there are plenty of edge cases I haven’t handled. I’ll leave it to you to decide if the syntax above is an improvement or not. There’s a certain nasty “pre-ISO C functions” feel to declaring the keywords before the method definition. But I like that the acceptable parameters are obvious and not hidden in the method definition.

Of course, none of this is a substitute for real language-level keyword params, which I hear we’ll be getting in Ruby 2.0. But in the meantime, it’s something to play with.

The code is here: https://github.com/avdi/keyword_params

Published by Avdi Grimm

8 Comments

  1. Nice article. I took a look to the code, clever solution 😉 I love ruby for stuff like that. You can write code that looks like part of the language itself.

    Reply
  2. For defaulting hash members, another way is:

        options.reverse_merge! :the => ‘cow’, :says => ‘moo’

    Reply
    • Indeed. ActiveSupport only, I believe, although you can just use #merge in plain ruby. I prefer #fetch because it gives me more control; I can lazily evaluate defaults or raise exceptions for missing required parameters.

      Reply
  3. “There’s a certain nasty “pre-ISO C functions” feel to declaring the keywords before the method definition.”

    This made me laugh out loud. C is my favorite language to be sure, but I do draw the line this side of K&R function definition.

    Reply
  4. Cool!  But you have a stray backslash in the link text to the code.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *