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.
November 12, 2007 at 11:21 pm
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.
July 11, 2008 at 7:32 am
Excellent tutorial ! It describes exactly that I wanted to do.
It’s clear and gradual … ruby way !