Metaprogramming in Ruby

Metaprogramming in Ruby enables you to produce elegant, clean, and beautiful programs as well as unreadable, complex code that’s not maintainable. This book teaches you the powerful metaprogramming concepts in Ruby, and how to use them judiciously.

6 min read

I recently read this book to improve my knowledge of Ruby and highly recommend it. It explains metaprogramming in a simple, easy-to-understand style.

The primary benefit of metaprogramming I found was not so much in writing clever code, but instead reading and understanding open source code, especially the Rails source code.

Rails makes abundant use of metaprogramming. Having the knowledge of these concepts makes it possible to read and understand the source code to gain a deeper appreciation of the Rails framework and unveil its magic.

The Object Model

Objects are first-class citizens in Ruby. You’ll see them everywhere. However, objects are part of a larger world that also includes other language constructs, such as classes, modules, and instance variables. All these constructs live together in a system called the object model.

In most programming languages, language constructs like variables, classes, methods, etc. are present while you are programming, but disappear before the program runs. They get transformed into byte-code (Java) or CIL (C#), or plain machine-code (C). You can’t modify these representations once the program has been compiled.

In Ruby, however, most language constructs are still there. You can talk to them, query them, manipulate them. This is called introspection. For example, the example below creates an instance of a class and asks for its class and instance variables.

class Language
  def initialize(name, creator)
    @name = name
    @creator = creator
  end
end

ruby = Language.new("Ruby", "Matz")

pp ruby.class	# Language
pp ruby.instance_variables	# [:@name, :@creator]

Open Classes

You can open any class and add new methods to it. For example, let’s open the String class and add a new method log on it.

class String
  def log
    puts ">> #{self}"
  end
end

"Hello World".log	# >> Hello World

This is called monkey-patching, and it can cause problems if you redefine existing methods unintentionally. However, if you know what you are doing, monkey-patching can be very powerful. For example, the ActiveSupport gem in Rails makes heavy use of monkey-patching to open Ruby core classes and define new functionality on them.

Instance Variables vs. Instance Methods

An object’s instance variables live in the object itself, and an object’s methods live in the object’s class.

Instance variables vs instance methods in Ruby
Source: Metaprogramming Ruby 2

That’s why objects of the same class share methods but don’t share instance variables.

Classes are Objects

Everything that applies to objects also applies to classes. Each class is also a module with three additional instance methods: new, allocate, and superclass.

class MyClass
  def my_method
    @v = 1
  end
end

puts MyClass.class  # Class
puts MyClass.superclass  # Object
puts Class.superclass  # Module
puts Object.class  # Class
puts Object.superclass  # BasicObject
pp BasicObject.superclass  # nil

What happens when you call a method?

When you call a method, Ruby does the following:

  1. Finds the method using method lookup. For this, Ruby interpreter looks into the receiver’s class, including the ancestor chain.
  2. Execute the method using self.

The receiver is the object that you call a method on, e.g. in the statement myObj.perform(), myObj is the receiver.

The ancestor chain is the path of classes from a class to its superclass, until you reach the root, i.e. BasicObject.

Ruby inheritance chain
Source: Metaprogramming Ruby 2

The Kernel

The Object class includes Kernel module. Hence the methods defined in Kernel are available to every object. In addition, each line in Ruby is executed inside a main object. Hence you can call the Kernel methods such as puts from everywhere.

If you add a method to Kernel, it will be available to all objects, and you can call that method from anywhere.

module Kernel
  def log(input)
    puts "Logging `#{input}` from #{self.inspect}"
  end
end

# Logging `hello` from main
log "hello" 

# Logging `a` from "hello"
"hello".log("a") 

# Logging `temp` from String
String.log("temp") 

The self Keyword

The Ruby interpreter executes each and every line inside an object - the self object. Here are some important rules regarding self.

  • self is constantly changing as a program executes.
  • Only one object can be self at a given time.
  • When you call a method, the receiver becomes self.
  • All instance variables are instance variables of self, and all methods without an explicit receiver are called on self.
  • As soon as you call a method on another object, that other object (receiver) becomes self.

At the top level, self is main, which is an Object. As soon as a Ruby program starts, the Ruby interpreter creates an object called main and all subsequent code is executed in the context of this object. This context is also called top-level context.

puts self  # main
puts self.class  # class

In a class or module definition, the role of self is taken by the class or module itself.

puts self  # main

class Language
  puts self  # Language

  def compile
    puts self  # #<Language:0x00007fc7c191c9f0>
  end
end

ruby = Language.new
ruby.compile

Defining Classes and Methods Dynamically

The Class constructor and the define_method allows you to generate classes and methods on the fly, as the program is running.

Language = Class.new do
  define_method :interpret do
    puts "Interpreting the code"
  end
end

# Interpreting the code
Language.new.interpret

Calling Methods Dynamically

When you call a method, you’re actually sending a message to an object.

my_obj.my_method(arg)

Ruby provides an alternate syntax to call a method dynamically, using the send method. This is called dynamic dispatch, and it’s a powerful technique as you can wait until the last moment to decide which method to call, while the code is running.

my_obj.send(:my_method, arg)

Missing Methods

When you call a method on an object, the Ruby interpreter goes into the object’s class and looks for the instance method. If it can’t find the method there, it searches up the ancestor chain of that class, until it reaches BasicObject. If it doesn’t find the method anywhere, it calls a method named method_missing on the original receiver, i.e. the object.

The method_missing method is originally defined in the BasicObject class. However, you can override it in your class to intercept and handle unknown methods.

class Language
  def interpret
    puts "Interpreting"
  end

  def method_missing(name, *args)
    puts "Method #{name} doesn't exist on #{self.class}"
  end
end

ruby = Language.new
ruby.interpret # Interpreting
ruby.compile # Method compile doesn't exist on Language

instance_eval

This BasicObject#instance_eval method evaluates a block in the context of an object.

class Language
  def initialize(name)
    @name = name
  end

  def interpret
    puts "Interpreting the code"
  end
end

puts "***instance_eval with object***"

ruby = Language.new "Ruby"

ruby.instance_eval do
  puts "self: #{self}"
  puts "instance variable @name: #{@name}"
  interpret
end

puts "\n***instance_eval with class***"

Language.instance_eval do
  puts "self: #{self}"

  def compile
    puts "Compiling the code"
  end

  compile
end

Language.compile

The above program produces the following output

***instance_eval with object***
self: #<Language:0x00007fc6bb107730>
instance variable @name: Ruby
Interpreting the code

***instance_eval with class***
self: Language
Compiling the code
Compiling the code

Class Definitions

A Ruby class definition is just regular code that runs. When you use the class keyword to create a class, you aren’t just dictating how objects will behave in the future. You are actually running code.

class MyClass
  puts "Hello from MyClass"
  puts self
end

# Output
# Hello from MyClass
# MyClass

class_eval()

Evaluates a block in the context of an existing class. This allows you to reopen the class and define additional behavior on it.

class MyClass
end

MyClass.class_eval do
  def my_method
    puts "#{self}"
  end
end

MyClass.new.my_method  # #<MyClass:0x00007f945e110b80>

A benefit of class_eval is that it will fail if the class doesn’t already exist. This prevents you from creating new classes accidentally.

Singleton Methods and Classes

You can define methods on individual objects, instead of defining them in the object’s class.

animal = "cat"

def animal.speak
  puts self
end

animal.speak  # cat

When you define the singleton method on the animal object, Ruby does the following:

  1. Create a new anonymous class, also called a singleton/eigenclass.
  2. Define the speak method on that class.
  3. Make this new class the class of the animal object.
  4. Make the original class of the object (String), the superclass of the singleton class.

animal -> Singleton -> String -> Object

Classes are objects, and class names are just constants. Calling a method on a class is the same as calling a method on an object.

The superclass of the singleton class of an object is the object’s class. The superclass of the singleton class of a class is the singleton class of the class’s superclass.

You can define attributes on a class as follows:

class Foo
  class << self
    attr_accessor :bar
  end
end

Foo.bar = "It works"
puts Foo.bar

Remember that an attribute is just a pair of methods. If you define an attribute on the singleton class, they become class methods.

Conclusion

Keep your code as simple as possible, and add complexity as you need it.

Though metaprogramming in Ruby looks like magic, it’s still just programming. It is so deeply ingrained in Ruby that you can barely write idiomatic Ruby without using a few metaprogramming techniques.

Ruby expects that you will change the object model, reopen classes, define methods dynamically, and create/execute code on-the-fly.

Writing perfect metaprogramming code up-front can be hard, so it’s generally easy to evolve your code as you go.

When you start, strive to make your code correct in the general cases, and simple enough that you can add more special cases later.