Dr Nic

Extending _why’s Creature class

Many Rubist’s first explanation of metaprogramming is by why the lucky stiff (_why)’s Why’s (Poignant) Guide to Ruby, chapter 6, section 3.

You go on a dragon-hunting, adventure game using sexy Ruby syntax (a domain-specific language/DSL for adventure games?). Here is some sample syntax for defining a monster class:

 class Dragon < Creature
   life 1340     # tough scales
   strength 451  # bristling veins
   charisma 1020 # toothy smile
   weapon 939    # fire breath
 end

The life, strength, charisma and weapon class methods are generated by a traits class method called against the Creature class (read the chapter).

class Creature
  traits :life, :strength, :charisma, :weapon
end

Read this chapter section many times and admire the beauty of the idea (and amuse yourself with his writing style!).

But there is one small improvement that could be made: currently, after setting the trait methods (e.g. life 1340 sets the life trait to 1340), you cannot access the class's trait values directly via their original trait method. That is, you cannot call Dragon.life to retrieve the value 1340.

This is due to a limitation of the define_method method being used. The relevant code from _why's book is:

   def self.traits( *arr )
     # 2. Add a new class method to for each trait.
     arr.each do |a|
       metaclass.instance_eval do
         define_method( a ) do |val|
           @traits ||= {}
           @traits[a] = val
         end
       end
     end

The method creator define_method uses a block to define the generated method body. The parameters for the block (val in the example above) become the arguments of the method once its been added to the class. That is, if we call traits :life on our Creature class, then a class method will be generated that requires one argument - the value of the trait. That is, it will generate the following method:

class Creature
  def life(val)
    @traits ||= {}
    @traits[:life] = val
  end
end

Now, back to the problem. How do we support the syntax Dragon.life? To achieve this, the generated method would need to look like:

class Creature
  def life(val = nil)
    @traits ||= {}
    return @traits[:life] if not val
    @traits[:life] = val
  end
end

That is, we need a default value for our method argument. But... blocks don't allow parameters to have default values. We cannot do the following:

         define_method( a ) do |val = nil|
           @traits ||= {}
           return @traits[a] if not val
           @traits[a] = val
         end

A pity, yes.

So, we need to generate our methods differently. The solution is as follows:

      metaclass.class_eval <<-EOS
        def #{a}(val=nil)
          @traits ||= {}
          return @traits[:#{a}] if not val
          @traits[:#{a}] = val
        end
      EOS

The eval methods allow a string to be passed to them. So, we shall pass it a string that defines a new method the old fashioned way: using the def method constructor. Thus it allows us to have default values for our arguments.

QED.

ALTERNATIVE from Chris @ Errtheblog.com

Use the splat! (*) to allow zero or more arguments. Pluck the first one off to represent the incoming argument or nil.

def self.traits( *arr )
  arr.each do |a|
    metaclass.instance_eval do
       define_method( a ) do |*val|
         val = val.first
         @traits ||= {}
         return @traits[a] if not val
         @traits[a] = val
       end
     end
  end
end

There are unanswered questions about its support for 2+ arguments (where we only need support for 0 or 1) that shall remain unanswered for the sake of this simple hack. But feel free to start throwing exceptions around if your users need protection from themselves.

Related posts:

  1. Cucumber: building a better World (object) How to write helper libraries for your Cucumber step definitions...
  2. Magic Multi-Connections: A “facility in Rails to talk to more than one database at a time” At this point in time there’s no facility in Rails...
  3. [ANN] Generating new gems for graceful goodliness I don’t like you [1]. You don’t share code....
  4. Extending Rails is like converting a Mini Cooper into a Rocket Car Not only is Ruby on Rails open sourced so all...
  5. Your favourite _why projects on one page I have 3 links on my “blog roll” on the...

4 Responses to “Extending _why’s Creature class”

  1. Chris says:

    Hi Dr Nic. Nice write up. Just wanted to mention that it’s possible, but some would argue less clean, to do optional parameters with a define_method block. Splat the block, yeah.

    def self.traits( *arr )
    arr.each do |a|
    metaclass.instance_eval do
    define_method( a ) do |*val|
    val = val.first
    @traits ||= {}
    return @traits[a] if not val
    @traits[a] = val
    end
    end
    end

  2. Chris says:

    Hey, formatting.

  3. Dr Nic says:

    Bad formatting! I’ll add the example to the body where formatting is respected.

    I’m glad blocks can take splatted arguments. I don’t think I knew they could til now. Cool.

  4. [...] Even ruby has problems, or, How to hack optional parameters for blocks [...]