Magic Beans in Ruby

November 12, 2007

One of the nice things about Ruby is how it lets you do ‘meta-programming’. Specifically, through the included(base), class_eval, and define_method methods available in the Module module, we can write code that seems to magically write other code for us!

Forces

Suppose we had to write a whole lot of classes that all needed to descend from a common base class. Suppose further, that these classes will all have several attributes (different per class), and that we need to initialize these all attributes at instantiation.

If we didn’t need to specifically descend from a common base class, we could just use Struct and perhaps factor out the common base-class functionality into a module or mixin. But then we’d still have to include the mixin with every class definition, and that’s not very DRY.

So, typically, we might have something like this:

class Base
end

class Foo < Base
  attr_accessor :foo
  def initialize(foo)
    self.foo = foo
  end
end

class Bar < Base
  attr_accessor :bar
  def initialize(bar)
    self.bar = bar
  end
end

As the number of our subclasses grows larger, it becomes more and more tedious and repetitive to keep writing the accessor lists and initializers like that. Suppose we just want to write something like:

class WidgetAbc < Base
  define_bean :a, :b, :c
end

A method to create methods

Let’s start with writing a method that creates our initializer for us.

We could create a single initializer that takes in a hash (and uses the hash key-values to initialize its instance attributes), but that doesn’t lend itself to brevity when we have more and more attributes. So we want an initializer that takes whatever values were passed to it and assigns them to the instance attributes in the same order as they were defined. Confused? Maybe the code will clear things up:

class Base

  def self.create_initializer(*attributes)
    define_method(:initialize) do |*values|
      (0...attributes.size).each {|i|
        self.send("#{attributes[i]}=", values[i])
      }
    end
  end

end

All that really says is, create a (class) method create_initializer that takes in a variable number of arguments. When this method is called (within a class definition), define an initialize method that also takes in a variable number of objects (values) and then, in turn, calls each attribute ‘setter’ passing it the corresponding value.

This lets us write:

class FooBar < Base
  attr_accessor :foo, :bar
  create_initializer :foo, :bar
end

Obviously, the call to attr_accessor and create_initializer can be combined, so let’s call this our define_bean method (in class Base):

  def self.define_bean(*attributes)
    attr_accessor *attributes
    create_initializer *attributes
  end

The asterisk (*) in front of *attributes and *values makes use of Ruby’s array expansion syntax – basically, as a method parameter, it lets us ‘group’ together all remaining parameters into a single array variable. When used in front of an array variable, it ‘expands’ the array as if we were passing each of its members individually to the called method. Without it, we’d be passing in and out a single array object, and none of the above would work.

Factoring out our Magic Bean

Now that we have a Base class with our convenience methods, what if we want to be able to do the same for other base classes? Well then, the next step is to simply create our MagicBean module which we can reuse as much as we want. For this to work, our module must be able to define the methods within the class definition itself (and not just in class instances).

This is where included(base) and class_eval come into play:

module MagicBean

  def self.included(base)
    base.class_eval do

      def self.create_initializer(*attributes)
        define_method(:initialize) do |*values|
          (0...attributes.size).each {|i|
            self.send("#{attributes[i]}=", values[i])
          }
        end
      end

      def self.define_bean(*attributes)
        attr_accessor *attributes
        create_initializer *attributes
      end

    end
  end

end

The use of Module.included(base) and Module.class_eval is probably one of my favorite Ruby ‘tricks’, and getting my head around it the first time took quite a while!

In a nutshell, Module.included(base) lets us write code that’s executed whenever our module (MagicBean) is included in any other class or module. As a side note, the following won’t work:

module IncludesMagicBean
  include MagicBean
end

class Base
  include IncludesMagicBean
end

This is because in the above case, MagicBean will work its magic (defines the class methods) in the module IncludesMagicBean and not in the class Base! Keep your eye out for that one.

Module.class_eval simply tells Ruby to execute the code in the block as if it was written in the context of the class or module you called it on. The following are equivalent:

class Widget
  attr_accessor :bah
  def bahbah
    self.bah = 'bah'
  end
end

and

class Widget
end

Widget.class_eval do
  attr_accessor :bah
  def bahbah
    self.bah = 'bah'
  end
end

Now, armed with our MagicBean we can rewrite our original classes as:

class Base
  include MagicBean
end

class Foo < Base
  define_bean :foo
end

class Bar < Base
  define_bean :bar
end

A lot shorter, definitely less repetitive, and arguably easier to read and maintain!

A note about performance

If you have instantiate a lot of these objects all the time, you might want to skip this technique and write everything by hand. Ad hoc testing on my end (Ruby 1.8.6 on a 2.2GHz Intel MacBook Pro on OS X 10.4.10) shows that object creation using the MagicBean methods is roughly 4.4 times slower than if the accessors and initializers were declared by hand.

Don’t say you haven’t been warned.

Advertisements

4 Responses to “Magic Beans in Ruby”


  1. Very nice. I haven’t touched Ruby’s meta-programming stuff because of Rails.

    I remembered Guy Naor’s comment on his recent talk at Exist that you will not be a good Rails developer if you are not a good Ruby developer.

    Anyway, thanks Alistair for the tutorial. 🙂

  2. Bruno Duyé Says:

    Excellent tutorial ! It describes exactly that I wanted to do.
    It’s clear and gradual … ruby way !

  3. Mel Schlappi Says:

    Excellent read.. definitely going to bookmark this site.

  4. rkumar Says:

    Really sweet. Thanks for adding the note about performance. Bookmarked…


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: