Metaprogramming enables you to produce elegant, clean, and beautiful programs as well as their exact opposite programs. This book will teach you the powerful metaprogramming concepts in Ruby, and how to use them judiciously.
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 pp ruby.instance_variables
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.
Metaprogramming is writing code that writes code. Specifically, Metaprogramming is writing code that manipulates language constructs at runtime, in turn manipulating the Ruby object model.
Open Classes
You can open any class and add new methods on it. For example, let's open 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 monkeypatching, and it can cause problems if you re-define existing methods unintentionally. However, if you know what you are doing, monkeypatching can be very powerful. For example, the ActiveSupport gem in Rails makes heavy use of monkeypatching 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?
- 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
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 on the BasicObject
class. However, you can override it in your class to intercept and handle the 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.
Takeaway
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.
Keep your code as simple as possible, and add complexity as you need it.
When you start, strive to make your code correct in the general cases, and simple enough that you can add more special cases later.
This post was originally published on my blog, where you can find my notes for other books I read. Hope you found it useful.
Let me know in the comments if you found any mistakes. I look forward to your feedback.
Top comments (0)