Metaprogramming in Ruby is even more impressive than its name suggests

It’s common to hear that metaprogramming is a technique reserved for Ruby experts and beyond the reach of everyday programmers. However, this is far from the truth. This blog post aims to debunk this misconception and make metaprogramming accessible to the average Ruby developer, allowing them to harness its power.

Ruby Metaprogramming: Code Writing Code

It’s important to note that metaprogramming can have a broad meaning and its usage can be prone to misuse and extremes. Therefore, I’ll focus on providing practical examples that demonstrate its everyday applications in programming.

Metaprogramming

Metaprogramming is a technique that allows you to write code that dynamically generates other code at runtime. This means you can define methods and classes on the fly. While it might sound complex, it essentially enables you to modify existing classes, handle non-existent methods by creating them dynamically, write more concise and DRY code by reducing repetition, and much more.

The Basics

Let’s start with the fundamentals of metaprogramming through an example. Consider the following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Developer

  def self.backend
    "I am backend developer"
  end
  
  def frontend
    "I am frontend developer"
  end

end

We’ve defined a class with two methods: a class method and an instance method. This is fundamental Ruby, but there’s a lot happening under the hood that we need to understand. It’s crucial to remember that the Developer class itself is an object. In Ruby, everything is treated as an object, including classes. Since Developer is an instance, it’s an instance of the Class class. Here’s a representation of the Ruby object model:

Ruby object model
1
2
3
4
p Developer.class # Class
p Class.superclass # Module
p Module.superclass # Object
p Object.superclass # BasicObject

Understanding the significance of self is key. The frontend method is a standard instance method available to Developer instances. However, the backend method is a class method. This is because every line of Ruby code is executed within a specific context, represented by self. The value of self points to the current object, which can vary depending on the code being executed. For instance, inside a class definition, self references the class itself, which is an instance of the Class class.

1
2
3
4
class Developer
  p self 
end
# Developer

Within instance methods, self refers to an instance of the class.

1
2
3
4
5
6
7
8
class Developer
  def frontend
    self
  end
end
 
p Developer.new.frontend
# #<Developer:0x2c8a148>

Within class methods, self refers to the class itself, the specifics of which will be discussed later in this article:

1
2
3
4
5
6
7
8
class Developer
  def self.backend
    self
  end
end

p Developer.backend
# Developer

This brings us to the concept of a class method. Before delving into that, we need to introduce metaclasses, also known as singleton classes or eigenclasses. The frontend class method we defined earlier is simply an instance method defined within the metaclass associated with the Developer object! A metaclass is a class that Ruby automatically creates and inserts into the inheritance hierarchy to house class methods, ensuring they don’t interfere with instances of the class.

Metaclasses

Every object in Ruby possesses its own metaclass, which is hidden from the developer but accessible. Since our Developer class is ultimately an object, it has its own metaclass. Let’s illustrate this by creating a String object and manipulating its metaclass:

1
2
3
4
5
6
7
8
example = "I'm a string object"

def example.something
  self.upcase
end

p example.something
# I'M A STRING OBJECT

Here, we’ve added a singleton method called something to the object. Unlike class methods, which are accessible to all instances of a class, singleton methods are specific to a single instance. Class methods are commonly used, while singleton methods are less prevalent. However, both types of methods are added to the object’s metaclass.

The previous example can be rewritten as follows:

1
2
3
4
5
6
7
example = "I'm a string object"

class << example
  def something
    self.upcase
  end
end

Despite the syntax difference, both achieve the same outcome. Now, let’s revisit our Developer class example and explore different ways to define a class method:

1
2
3
4
5
class Developer
  def self.backend
    "I am backend developer"
  end
end

This is the standard definition used by most developers.

1
2
3
def Developer.backend
  "I am backend developer"
end

This is equivalent to the previous definition. We define the backend class method for Developer without explicitly using self. Defining a method in this manner automatically makes it a class method.

1
2
3
4
5
6
7
class Developer
  class << self
    def backend
      "I am backend developer"
    end
  end
end

Here, we define a class method using syntax similar to defining a singleton method for a String object. Note the use of self, which refers to the Developer object. We open the Developer class, making self equal to the Developer class. Next, we use class << self, setting self to Developer’s metaclass. Finally, we define the backend method within Developer’s metaclass.

1
2
3
4
5
class << Developer
  def backend
    "I am backend developer"
  end
end

This block sets self to Developer’s metaclass for its duration. Consequently, the backend method is added to Developer’s metaclass instead of the class itself.

Let’s examine how the metaclass behaves within the inheritance tree:

Metaclass in inheritence tree

Previous examples haven’t provided concrete evidence of the metaclass’s existence. However, we can use a trick to reveal this hidden class:

1
2
3
4
5
6
7
class Object
  def metaclass_example
    class << self
      self
    end
  end
end

We define an instance method within the Object class. Inside this method, self refers to the Object object. We then use class << self to change the current self to point to the metaclass of the current object. Since the current object is the Object class, this would be the instance’s metaclass. The method returns self, which represents the metaclass. By calling this instance method on any object, we can obtain its metaclass. Let’s recreate our Developer class and explore further:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Developer

  def frontend
    p "inside instance method, self is: " + self.to_s
  end

  class << self
    def backend
      p "inside class method, self is: " + self.to_s
    end
  end
  
end

developer = Developer.new
developer.frontend
# "inside instance method, self is: #<Developer:0x2ced3b8>"

Developer.backend
# "inside class method, self is: Developer"

p "inside metaclass, self is: " + developer.metaclass_example.to_s
# "inside metaclass, self is: #<Class:#<Developer:0x2ced3b8>>"

To demonstrate that frontend is an instance method of a class and backend is an instance method of a metaclass, consider the following:

1
2
3
4
5
p developer.class.instance_methods false
# [:frontend]

p developer.class.metaclass_example.instance_methods false
# [:backend]

It’s important to note that you don’t need to reopen Object and add this hack to access the metaclass. Ruby provides the singleton_class method for this purpose. While functionally equivalent to our metaclass_example hack, it provides insights into Ruby’s internal workings:

1
2
p developer.class.singleton_class.instance_methods false
# [:backend]

Defining Methods Using “class_eval” and “instance_eval”

Another way to create a class method is using instance_eval:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Developer
end

Developer.instance_eval do
  p "instance_eval - self is: " + self.to_s
  def backend
    p "inside a method self is: " + self.to_s
  end
end
# "instance_eval - self is: Developer"

Developer.backend
# "inside a method self is: Developer"

The Ruby interpreter evaluates this code within the context of an instance, which is the Developer object in this case. When defining a method on an object, you create either a class method or a singleton method. In this instance, it’s a class method—more precisely, class methods are singleton methods of a class, while others are singleton methods of an object.

Conversely, class_eval evaluates code within the context of a class instead of an instance. It effectively reopens the class. Here’s how class_eval can be used to create an instance method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Developer.class_eval do
  p "class_eval - self is: " + self.to_s
  def frontend
    p "inside a method self is: " + self.to_s
  end
end
# "class_eval - self is: Developer"

p developer = Developer.new
# #<Developer:0x2c5d640>

developer.frontend
# "inside a method self is: #<Developer:0x2c5d640>"

In essence, class_eval changes self to refer to the original class, while instance_eval changes self to refer to the original class’s metaclass.

Defining Missing Methods on the Fly

Another crucial aspect of metaprogramming is the concept of method_missing. When a method is called on an object, Ruby searches for the method within the class’s instance methods. If not found, it continues up the ancestors chain. If the method remains unfound, Ruby calls the method_missing method, an instance method of Kernel inherited by every object. Since Ruby consistently calls this method for missing methods, we can leverage it for various purposes.

define_method is a method defined in the Module class used for dynamic method creation. It takes the new method’s name and a block, where the block’s parameters become the new method’s parameters. While both def and define_method create methods, define_method can be used in conjunction with method_missing to write DRY (Don’t Repeat Yourself) code. More specifically, define_method can manipulate scopes when defining a class, but that’s beyond the scope of this discussion. Let’s illustrate with a simple example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Developer
  define_method :frontend do |*my_arg|
    my_arg.inject(1, :*)
  end

  class << self
    def create_backend
      singleton_class.send(:define_method, "backend") do
        "Born from the ashes!"
      end
    end
  end
end

developer = Developer.new
p developer.frontend(2, 5, 10)
# => 100

p Developer.backend
# undefined method 'backend' for Developer:Class (NoMethodError)

Developer.create_backend
p Developer.backend
# "Born from the ashes!"

This demonstrates how define_method creates an instance method without using def. However, we can achieve much more. Consider this code snippet:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Developer

  def coding_frontend
    p "writing frontend"
  end

  def coding_backend
    p "writing backend"
  end

end

developer = Developer.new

developer.coding_frontend
# "writing frontend"

developer.coding_backend
# "writing backend"

This code is not DRY. We can improve it using define_method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Developer

  ["frontend", "backend"].each do |method|
    define_method "coding_#{method}" do
      p "writing " + method.to_s
    end
  end

end

developer = Developer.new

developer.coding_frontend
# "writing frontend"

developer.coding_backend
# "writing backend"

This is better but not perfect. If we want to add a new method, such as coding_debug, we need to manually add "debug" to the array. We can address this using method_missing:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Developer

  def method_missing method, *args, &block
    return super method, *args, &block unless method.to_s =~ /^coding_\w+/
    self.class.send(:define_method, method) do
      p "writing " + method.to_s.gsub(/^coding_/, '').to_s
    end
    self.send method, *args, &block
  end

end

developer = Developer.new

developer.coding_frontend
developer.coding_backend
developer.coding_debug

Let’s break down this code. Calling a non-existent method triggers method_missing. We only want to create a new method if the method name starts with "coding_". Otherwise, we call super to handle the truly missing method. We use define_method to create the new method. This allows us to create numerous methods starting with "coding_", making our code significantly DRYer. Since define_method is private to Module, we use send to invoke it.

Wrapping up

This is just a glimpse into the world of metaprogramming. To become a Ruby expert, mastering these building blocks is essential. Once you grasp these concepts, you can explore more advanced topics like creating your own Domain-Specific Language (https://en.wikipedia.org/wiki/Domain-specific_language) (DSL). While DSLs are a separate topic altogether, these fundamental concepts are crucial for understanding them. Many widely used gems in Rails, such as RSpec and ActiveRecord, are built using DSLs, which you’ve likely used without realizing it.

Hopefully, this article provides a stepping stone towards understanding metaprogramming and perhaps even building your own DSLs, enabling you to write more efficient and expressive code.

Licensed under CC BY-NC-SA 4.0