Simulating Keyword Arguments






Simulating Keyword Arguments

Problem

A function or method can accept many optional arguments. You want to let callers pass in only the arguments they have values for, but Ruby doesn't support keyword arguments as Python and Lisp do.

Solution

Write your function to accept as its final argument a map of symbols to values. Consult the map as necessary to see what arguments were passed in.

	def fun_with_text(text, args={})
	  text = text.upcase if args[:upcase]
	  text = text.downcase if args[:downcase]
	  if args[:find] and args[:replace]
	    text = text.gsub(args[:find], args[:replace])
	  end
	  text = text.slice(0, args[:truncate_at]) if args[:truncate_at]
	  return text
	end

Ruby has syntactic sugar that lets you define a hash inside a function call without putting it in curly brackets. This makes the code look more natural:

	fun_with_text("Foobar", {:upcase => true, :truncate_at => 5})
	# => "FOOBA"
	fun_with_text("Foobar", :upcase => true, :truncate_at => 5)
	# => "FOOBA"
	fun_with_text("Foobar", :find => /(o+)/, :replace => '\1d', :downcase => true)
	# => "foodbar"

Discussion

This simple code works well in most cases, but it has a couple of shortcomings compared to "real" keyword arguments. These simulated keyword arguments don't work like regular arguments because they're hidden inside a hash. You can't reject an argument that's not part of the "signature," and you can't force a caller to provide a particular keyword argument.

Each of these problems is easy to work around (for instance, does a required argument really need to be a keyword argument?), but it's best to define the workaround code in a mixin so you only have to do it once. The following code is based on a KeywordProcessor module by Gavin Sinclair:

	###
	# This mix-in module lets methods match a caller's hash of keyword
	# parameters against a hash the method keeps, mapping keyword
	# arguments to default parameter values.
	#
	# If the caller leaves out a keyword parameter whose default value is
	# :MANDATORY (a constant in this module), then an error is raised.
	#
	# If the caller provides keyword parameters which have no
	# corresponding keyword arguments, an error is raised.
	#
	module KeywordProcessor
	  MANDATORY = :MANDATORY

	  def process_params(params, defaults)
	    # Reject params not present in defaults.
	    params.keys.each do |key|
	      unless defaults.has_key? key
	        raise ArgumentError, "No such keyword argument: #{key}"
	      end
	    end
	    result = defaults.dup.update(params)

	    # Ensure mandatory params are given.
	    unfilled = result.select { |k,v| v == MANDATORY }.map { |k,v| k.inspect }
	    unless unfilled.empty?
	      msg = "Mandatory keyword parameter(s) not given: #{unfilled.join(', ')}"
	      raise ArgumentError, msg
	    end

	    return result
	  end
	end

Here's KeywordProcessor in action. Note how I set a default other than nil for a keyword argument, by defining it in the default value of args:

	class TextCanvas
	  include  
KeywordProcessor

	  def render(text, args={}.freeze)
	    args = process_params(args, {:font => 'New Reykjavik Solemn', :size => 36,
	                                 :bold => false, :x => :MANDATORY,
	                                 :y => :MANDATORY }.freeze)
	    # …
	    puts "DEBUG: Found font #{args[:font]} in catalog."
	    # …
	  end
	end

	canvas = TextCanvas.new

	canvas.render('Hello', :x => 4, :y => 100)
	# DEBUG: Found font New Reykjavik Solemn in catalog.

	canvas.render('Hello', :x => 4, :y => 100, :font => 'Lacherlich')
	# DEBUG: Found font Lacherlich in catalog.

	canvas.render('Hello', :font => "Lacherlich")
	# ArgumentError: Mandatory keyword parameter(s) not given: :x, :y

	canvas.render('Hello', :x => 4, :y => 100, :italic => true)
	# ArgumentError: No such keyword argument: italic

Ruby 2.0 will, hopefully, have full support for keyword arguments.

See Also

  • Recipe 8.8, "Delegating Method Calls to Another Object"

  • The KeywordProcessor module is based on the one in "Emulating Keyword Arguments in Ruby"; I modified it to be less oriented around the initialize method (http://www.rubygarden.org/ruby?KeywordArguments)



 Python   SQL   Java   php   Perl 
 game development   web development   internet   *nix   graphics   hardware 
 telecommunications   C++ 
 Flash   Active Directory   Windows