MetaRuby - An Ounce of Meta


Metaprogramming sounds hand wavy and bewildering to a lot of folks. I thought it would be fun to do a series that shows metaprogramming isn’t some dark art, or esoteric skill, but simply a handy tool any Rubyist can master.

Metaprogramming is writing code that writes code. That's it.

You are almost certainly using Ruby code that writes code today, let’s work through one very simple example to whet your appetite.

Your Own attr_reader

Ruby provides some really handy methods for us like attr_reader and attr_writer. When you call attr_reader :name, :phone, it essentially writes the following code for you:

class Person
  # attr_reader :name, :phone
  
  # attr_reader(:name) generates
  def name 
    @name  
  end
  
  # attr_reader(:phone) generates
  def phone
    @phone
  end
end

To make our own attr_reader, we need to define an instance method for each symbol passed in. That instance method should return its corresponding instance variable. Ruby provides two methods we can use to achieve this: define_method and instance_variable_get.

Putting those methods together we end up with the following code:

module MyAttr
  def my_reader(attr_name)
    # Defines the method named by `attr_name`
    define_method(attr_name) do
      # Get the instance variable named by `attr_name`
      instance_variable_get("@"+attr_name.to_s)
    end
  end
end

For now we will just stick this in a Module named MyAttr. Let’s try it out by exposing my_reader as a class method:

class Person
  # Use extend instead of include so my_reader is a class method.
  extend MyAttr
  my_reader :name
  
  def initialize(name)
    @name = name
  end
end

person = Person.new("Mortimer")
person.name
#=> "Mortimer"

We just implemented a method that writes methods, great huh? Let’s take this one step further so that we can define multiple readers at once just like attr_reader:

module MyAttr
  def my_reader(*attr_names)
    attr_names.each do |name|
      define_method(name) do
        instance_variable_get("@"+name.to_s)
      end
    end
  end
end

*attr_names lets us pass in zero or more attribute names. For each of those, a corresponding method is defined.

Recap

By using a few simple Ruby methods, we have replicated Ruby’s attr_reader.

  • define_method defines a method on the caller.
  • instance_variable_get gets an instance variable.
  • Calling extend with a Module, exposes the module’s methods as class methods.

This is just a tiny taste of what you can easily do in Ruby. Want to go further? Try implementing a reader method that coerces its input to a given type, or writing the corresponding attr_writer.

Next time we’ll get into some more complex examples

More articles in this series