One feature Ruby sorely lacks is named parameter passing; we want the following behavior:
obj.setprice( apples=20, bananas=30, apricots=40 )
Where the parameters (apples, bananas, etc) may be specified in any order or not at all. The above code does not work in Ruby but we can fake it somewhat using a hash (Ruby 1.8 syntax):
obj.setprice ( :apples => 20, :bananas => 30, :apricots => 40 )
The hash syntax isn’t too bad, and is improved further in Ruby 1.9, however an alternative is to use the following DSL, tentatively called ‘Options’; the syntax is as follows:
obj.setprice { apples 20; bananas 30; apricots 40 }
Although the Options syntax is quite clean it is possibly overkill for most applications as it utilizes a block rather than just a hash. A better application of Options would be to provide parameter ‘chunking’. ‘Chunking’ is a mechanism for dividing up large parameter lists into manageable chunks according to conceptual (or other) boundaries and sending each chunk separately to the method for processing. As an example say our method was ‘obj.set_user_info’ and it requires many parameters relating to user height, weight, age, income, marital status etcetera; using Options we could send that data to the method in chunks, instead of one long list:
obj.set_user_info {
financial :income => 50000, :assets => 100000
physical :height => 170, :weight => 80
family :married => true, :children => 3
....
}
What does the code in set_user_info look like?
def set_user_info(&block)
financial, physical, family = *Options.new{financial, physical, family}.run &block
# do stuff with contents of financial, physical, family variables below
end
The parameters passed to Options.new define the ‘named parameters’ or ‘chunk names’ (depending on your application).
It is also possible to specify default values for the parameters:
x, y= *Options.new{x 50; y}.run &block
In the above, if ‘x’ is not given a value by the block it will default to 50; if ‘y’ is not given a value, however, it will evaluate to nil (as it has no default).
Here is the Ruby source code for Options:
class Options
def initialize(*argv, &b)
@valid_params = []
@hash_map = {}
@init_phase = true
instance_eval &b
@init_phase = false
end
def method_missing(name, *argv)
case @init_phase
when true
@valid_params << name
when false
raise ArgumentError, "#{name} not a defined name parameter" if !@valid_params.include? name
end
@hash_map[name] = argv.size > 1 ? argv : argv.first
end
def run(&b)
instance_eval &b
self
end
def to_a
@hash_map.values_at(*@valid_params)
end
end
And here is the Ruby C API source code for Options (just for fun):
#include <ruby.h>
#include <stdio.h>
#include <string.h>
VALUE jm_Class;
static VALUE
co_initialize(VALUE self, VALUE args) {
rb_need_block();
rb_iv_set(self, "@valid_params", rb_ary_new());
rb_iv_set(self, "@param_hash", rb_hash_new());
rb_iv_set(self,"@init_phase", Qtrue);
rb_obj_instance_eval(0,0, self);
rb_iv_set(self,"@init_phase", Qfalse);
return Qnil;
}
static VALUE
co_method_missing(VALUE self, VALUE argv) {
VALUE param_name = rb_ary_shift(argv);
VALUE valid_params = rb_iv_get(self, "@valid_params");
VALUE param_hash = rb_iv_get(self, "@param_hash");
VALUE init_phase = rb_iv_get(self, "@init_phase");
switch(init_phase) {
case Qtrue:
rb_ary_push(valid_params, param_name);
break;
case Qfalse:
if(!rb_ary_includes(valid_params, param_name))
rb_raise(rb_eArgError, "%s is not a valid named parameter", rb_id2name(SYM2ID(param_name)));
break;
}
argv = RARRAY(argv)->len > 1 ? argv : rb_ary_entry(argv, 0);
rb_hash_aset(param_hash, param_name, argv);
return Qnil;
}
static VALUE
co_run(VALUE self) {
rb_need_block();
rb_obj_instance_eval(0, 0, self);
return self;
}
static VALUE
co_to_a(VALUE self) {
VALUE valid_params = rb_iv_get(self, "@valid_params");
VALUE param_hash = rb_iv_get(self, "@param_hash");
int len = RARRAY(valid_params)->len;
return rb_hash_values_at(len, RARRAY(valid_params)->ptr, param_hash);
}
void Init_options()
{
jm_Class = rb_define_class("Options", rb_cObject);
rb_define_method(jm_Class,"initialize",co_initialize,-2);
rb_define_method(jm_Class,"method_missing",co_method_missing,-2);
rb_define_method(jm_Class,"to_a", co_to_a,0);
rb_define_method(jm_Class,"run", co_run,0);
}
UPDATE: Options should not be considered a replacement for the hash syntax for named parameters. The hash syntax is, in most cases, a better solution. Options may however be useful in a small number of applications.
Filed under: metaprogramming, programming, ruby | Tagged: dsl, named parameters, ruby, ruby C API