Ethan Fast

Metaprogramming Ruby

9 December, 2010 -- Vienna, VA

I just finished the book Metaprogramming Ruby and I’ve stepped away with a much deeper appreciation for some attributes of the Ruby language. I’m a fan of Clojure, so I was pleased to discover that Ruby’s object model has a dynamism that begs comparison to the data-is-code philosophy of more lispy languages. For more on this (although I don’t agree with all the article’s conclusions), read Why Ruby is an Acceptable Lisp.

I’ve written some code to demonstrate a few of Ruby’s interesting features. In particular, consider the class FStore:

class FStore  
    attr_accessor :dynamic_methods  
    def initialize  
        dynamic_methods = []
    end
    def mix(s1, s2, s3, &block)
        name = "#{s1.to_s}_#{s2.to_s}_#{s3.to_s}".to_sym
        self.send(name) {|a,b| send(s1, send(s2,a), send(s3,b))}
    end
    def method_missing(symbol, &block)
        if block_given? 
            self.class.send :define_method, symbol, &block
        else
            bdy = proc {|*x| symbol}
            self.class.send :define_method, symbol, bdy
        end
        dynamic_methods << symbol  
    symbol  
    end  
end

FStore is a very small DST (Domain Specific Language), and with it I can declare methods in a new way:

# New object  
e = FStore.new

# Define a bunch of methods dynamically  
e.double {|x| 2*x}  
e.triple {|x| 3*x}  
e.square {|x| x**2}  
e.plus {|x,y| x + y}

# Then you can use them, like so:  
e.double 2 #=> 4  
e.triple 2 #=> 6

To understand how this works, look to method_missing, as it does all the heavy lifting:

def method_missing(symbol, &block)  
    if block_given?  
        self.class.send :define_method, symbol, &block  
    else  
        bdy = proc {|*x| symbol}  
        self.class.send :define_method, symbol, bdy  
    end  
    @dynamic_methods << symbol  
    symbol  
end

With that, here’s the basic idea:

  1. The ruby interpreter attempts to evaluate a statement (e.g. “e.double {|x| 2*x}”)
  2. On method lookup, no double method is present for e or its ancestors, so the interpreter passes double (as a symbol) and its block to method_missing.
  3. Normally, method_missing would return an error, but we’ve overridden it for the FStore class. Instead, it creates a new instance method named double. The body of this method is the same block that we initially passed e.double.

So to construct methods dynamically you can change the inherited behavior of method_missing. But what’s the deal with send? In method_missing, we use it to declare a new instance method:

# This is what we did
self.class.send :define_method, symbol, &block
# This, although seemingly equivalent, would not work
# because define_method is private
define_method symbol, &block
# Another example of using send
[1,2,3].first # is equivalent to
[1,2,3].send(:first)

This isn’t complicated. The send method provides another way to call a method on an object. Why then did we use it instead of calling define_method directly? Well, with send we can call an object’s private methods.

Now, let’s move on to some other interesting abilities of FStore:

# Mix a few methods
e.mix(:plus, :double, :triple)
    # => create: def plus_double_triple a,b ; plus (double a) (triple b) ; end
# We can call the new method
e.plus_double_triple 2, 3 #=> (plus (double 2) (triple 3)) => 13

Here, we call mix to generate a new instance method (prefix style) out of e.plus, e.double, and e.triple. Mix will call this plus_double_triple, and we can use it just like any other method. Let’s look back to the definition of mix.

def mix(s1, s2, s3, &block) 
  name = "#{s1.to_s}_#{s2.to_s}_#{s3.to_s}".to_sym
  self.send(name) {|a,b| send(s1, send(s2,a), send(s3,b))}
end

Mix creates a new name out of the three method names that we pass it, and defines a new method with that name using a hierarchy of sends. The method definition itself is delegated once to method_missing, as the outermost send passes a block to plus_double_triple, which doesn’t initially exist for our FStore instance.

A complete gist (with all the referenced code) is available here.