Wednesday, June 27, 2007

Using of keyword based parameter

I plan to have presentation of Ruby Language in enterprise application development for developers of the company I worked for and I am thinking about which message should I pass to them. Instead of trying to give many reasons that others already talked about huge benefit of using Ruby, I decide to give my own very personal reasons why I love Ruby based on my one and half year working with this language.
In early day, when I programed in PL/SQL, I know that PL/SQL support keyword based parameter in procedure/function but I never used this feature, simply I have not recognized the benefit of using it that time.
Ruby does not support keyword based parameter but Ruby programmers fake it easily using hash in combination with symbol. Using this style is very popular in Ruby core lib and Rails. The following example illustrates it, suppose we want to create method create_user that create database user, in traditional position based parameter, we will do like that
def create_user(username,password,ignore_error,force,verbose)
   #.. implementation detail is ignored
end
#calling it
create_user('scott','tiger',false,true,true)
on keyword based version, the method has simply one parameter params
def create_user(params)
   username = params[:username]
   password = params[:password]
   ignore_error = params[:ignore_error]
   verbose = params[:verbose]
   force= params[:force]
   #.. implementation detail is ignore
end
#calling it
create_user(:username=>'scott',:password=>'tiger',:ignore_error=>false,
  :verbose=>true, :force=>true)
The keyword based version is obviously more verbose, requires more typing, but offers several benefits.

MORE EXPRESSIVE AND LESS ERROR
The keyword based version is more expressive, just by looking at how the method is called, we know what is the intention, there is no need to look at the implementation file to figure out what it does. The position based version suffers what Joshua Bloch mentioned in his How to Design a Good API and Why it Matters "Long lists of identically typed params harmful"

GOOD DEFAULT
In the position based version, we can only assign default value for those parameters that are at the end of parameter list.
def create_user(username,password=username,ignore_error=false,force=false,verbose=false)
   #.. implementation detail is ignored
end
#calling it
create_user('scott')
This will not give a flexibility of using default value just for few last one. In the keyword based version, we can archive it easily as follow
def create_user(params)
   username = params[:username]
   password = params[:password] || username
   ignore_error = params[:ignore_error] 
   verbose = params[:verbose]
   force= params[:force]
   
   #implementation detail is ignored
   
   #note that Ruby consider nil as false in condition expression, so we should design 
   #boolean value keyword in such way that its default value is false
end

#calling it
create_user(:username=>'scott',:verbose=>true)
create_user(:username=>'scott',:force=>true)
create_user(:username=>'scott',:ignore_error=>true)
I also see people using Hash::merge to shorten assignment of default values and adding some assertion of accepted keywords e.g
#borrow from ActiveSupport
class Hash
  def assert_valid_keys(*valid_keys)
    unknown_keys = keys - [valid_keys].flatten
    raise(ArgumentError,
    "Unknown key(s): #{unknown_keys.join(", ")}\nValid key(s): #{valid_keys.join(',')}")   
      unless unknown_keys.empty?
  end    
end

class DatabaseSchemaBuilder
  
  attr_accessor :ignore_error,:verbose,:force

  def default_options
    {:ignore_error=>@ignore_error,:verbose=>@verbose,:force=>@force}
  end

  def create_user(params)
    params.assert_valid_keys(:ignore_error,:verbose,:force)
    params = default_options.merge(params)
   
    username = params[:username]
    password = params[:password] 
    ignore_error = params[:ignore_error]
    verbose = params[:verbose]
    force= params[:force]

   #implementation detail is ignored
  end
end
EASY TO CHANGE
When we need lets say adding new parameter to the method. In position based version, we end up with changing contract of the method, which may result in looking at every line of code that use this method and make change unless you put at the end of parameter list with default value.
In keyword based version, it is obvious less painful, just adding one more keyword, setting a default value, anyway change only the method itself, e.g. we want to add parameter :noop, meaning no operation, just for testing.
def default_options
   {:ignore_error=>false,:verbose=>false,:force=>false,:noop=>false}
end

def create_user(params)
   params.assert_valid_keys(:ignore_error,:verbose,:force,:noop)
   params = default_options.merge(params)
   
   username = params[:username]
   password = params[:password] || username
   ignore_error = params[:ignore_error]
   verbose = params[:verbose]
   force= params[:force]
   noop=params[:noop]

   #implementation detail is ignored
end
HIDING DESIGN DECISION
Using position based parameter with little bit long parameter list, I have to decide if put one parameter before other or not. I do not have this problem when using keyword based parameter, so it help me do program faster.

No comments: