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.
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:
- Finds the method using method lookup. For this, Ruby interpreter looks into the receiver’s class, including the ancestor chain.
- 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
.
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 onself
. - 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:
- Create a new anonymous class, also called a singleton/eigenclass.
- Define the
speak
method on that class. - Make this new class the class of the
animal
object. - 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.