Metaprogramming in Ruby

rubyrailsSeptember 19, 2023Dotby Alkesh Ghorpade

Metaprogramming, as per Wikipedia, is defined as

A programming technique in which computer programs have the ability to treat other programs as their data. It means that a program can be designed to read, generate, analyze or transform other programs, and even modify itself while running. In some cases, this allows programmers to minimize the number of lines of code to express a solution, in turn reducing development time.

Metaprogramming is a powerful technique but can also be complex and challenging to master. Programmers often use it to write more efficient, flexible, and adaptable code.

A few examples where metaprogramming gets used are:

  1. Compiler
  2. Debugger
  3. Unit Testing framework

What is Metaprogramming in Ruby?

Ruby, being an object-oriented and dynamic programming language, has built-in support for metaprogramming techniques. Metaprogramming in Ruby can be used in a variety of ways:

  1. Dynamically defining methods and classes
  2. Modifying existing classes and methods
  3. Inspecting and manipulating code objects

Metaprogramming makes Ruby code more powerful, flexible, and expressive. It can also create new features and functionality for the Ruby language itself.

Metaprogramming in Ruby can help DRY up code and help developers write reusable code or create a library that can be extracted as a gem. Rails relies heavily on metaprogramming, which is why it is often described as magical.

Metaprogramming methods

Let's explore the methods send, define_method, and method_missing with real life scenarios and how metaprogramming makes Ruby code more powerful, flexible, and expressive.

send method

The send method in Ruby is used to dynamically call methods on objects. It takes two arguments: the name of the method to call and any arguments that should be passed to the method. The method name can be passed as a string or a symbol.

Let's take a Rails application where it tracks the order shipment status. The application has a function where it calls the respective action on the order shipment.

def update_order_shipment_status(order_shipment, action)
  if action == "initiated"
    order_shipment.initiated
  elsif action == "dispatched"
    order_shipment.dispatched
  elsif action == "in_transit"
    order_shipment.in_transit
  elsif action == "out_for_delivery"
    order_shipment.out_for_delivery
  elsif action == "delivered"
    order_shipment.delivered
  elsif action == "cancelled"
    order_shipment.cancelled
  end
end

The method would need a change whenever a new order shipment status is added. The multiple if..else clause can be changed by replacing the code using send.

def update_order_shipment_status(order_shipment, action)
  return if !order_shipment.respond_to?(action)

  order_shipment.send(action)
end

Note:

The send method can call both the private and the public methods. Developers must carefully add it to avoid exposing the class's private methods. You can use the public_send method to call only the public methods.

define_method

Let's consider the order shipment Rails application. If you want to check the order shipment status, you might implement the methods below in the OrderShipment model.

class OrderShipment < ApplicationRecord
  def is_initiated?
    self.status == "initiated"
  end

  def is_dispatched?
    self.status == "dispatched"
  end

  def is_in_transit?
    self.status == "in_transit"
  end

  def is_out_for_delivery?
    self.status = "out_for_delivery"
  end
end

Instead of repeating the code, you can use the metaprogramming function provided by Ruby, define_method.

Using the define_method, you can create all the functions dynamically, as shown below:

class OrderShipment < ApplicationRecord
  STATUSES = ["initiated", "dispatched", "in_transit", "out_for_delivery", "delivered", "cancelled"].freeze

  STATUSES.each do |status|
    define_method("is_#{status}?") do
      self.status == status
    end
  end
end

The define_method will create the methods for all the status values defined in the STATUSES constant. The methods will be is_initiated?, is_dispatched?, is_in_transit?, etc and you need to call them on the OrderShipment object.

method_missing

One of the best examples of using method_missing in Rails is implementation of dynamic named scopes. Named scopes allow you to define reusable queries on your models, which can be used in your controllers and views.

Let's consider the OrderShipment example, where you add the below scopes:

class OrderShipment < ApplicationRecord
  scope :fetch_all_initiated_shipments, -> { where(status: "initiated") }
  scope :fetch_all_dispatched_shipments, -> { where(status: "dispatched") }
  ...
end

You might need to add a new scope whenever a new status gets added. You can avoid this by implementing a method_missing function.

class OrderShipment < ApplicationRecord
  STATUSES = ["initiated", "dispatched", "in_transit", "out_for_delivery", "delivered", "cancelled"].freeze

  def method_missing(method_name, *args)
    matched_expression = method_name.to_s.match(/^fetch_all_(?<status>.*)_shipments$/)
    status = matched_expression[:status]

    if STATUSES.include?(status)
      where(status: status)
    end
  end
end

The fetch_all_initiated_shipments is not added in the OrderShipment class. This will call the method_missing function. The function checks the method_name with a Regex /^fetch_all_(?<status>.*)_shipments$/.

If the method name starts with fetch_all_ and ends with _shipments, the regular expression will extract the text between these words. It considers the match expression as the status. You need to verify whether the status value matched in Regex is present in the STATUSES constant. If present, you can then query the OrderShipment model based on the status value.

Note

You can eliminate the metaprogramming code in the send, define_method and method_missing sections with Rails ActiveRecord::Enum.

Monkey Patching

Monkey patching in Ruby is a metaprogramming technique that allows you to modify the behaviour of existing classes and modules at runtime. It can be done by reopening the class or module and adding or overriding methods.

Here is an example of using monkey patching:

class String
  def reverse
    super.split("").reverse.join("")
  end
end

puts "hello, world".reverse
=> dlrow ,olleH

In the above example, the String class #reverse method is overridden. The new reverse method splits the string into characters, reverses the order of the characters, and then joins the characters back into a string.

How metaprogramming in Ruby is helpful

Metaprogramming can offer several benefits for Ruby developers, including:

  • More dynamic and flexible code: Metaprogramming allows developers to write code that can adapt to changing requirements. This can be useful for developing complex applications that need to be able to handle a wide range of scenarios.

  • Reduced duplication of code: Metaprogramming can reduce code duplication by allowing developers to define generic code that can be reused for different purposes.

  • Testing and Debugging: Metaprogramming can also simplify testing and debugging by creating mock objects or stubs that simulate the behaviour of real objects. This can be useful for isolating a particular object or service's behaviour and testing its interactions with other objects.

Metaprogramming in Rails

Metaprogramming is widely used in Rails to implement a variety of features, including:

Dynamic routing: Rails uses metaprogramming to generate routes based on the controllers and models in your application. This allows you to create dynamic and flexible URLs for your application.

Scaffolding: Rails uses metaprogramming to generate scaffolding code for your models. This scaffolding code includes basic controllers, views, and tests, which can save you a lot of time and effort when developing new applications.

Model validation: Rails uses metaprogramming to implement model validation. This allows you to easily define validation rules for your models and ensure that your data is valid before it is saved to the database.

Active Record: Active Record, the Rails ORM, uses metaprogramming extensively to implement its features. For example, Active Record uses metaprogramming to generate SQL queries, manage database connections, and implement object-relational mapping.

Associations: Rails associations, such as has_many and belongs_to, are implemented using metaprogramming. This allows you to easily define relationships between your models and generate the necessary SQL code to manage those relationships.

Callbacks: Rails callbacks, such as before_save and after_create, are implemented using metaprogramming. This lets you quickly hook into the model lifecycle and execute custom code at specific points.

In addition to these specific features, Rails also uses metaprogramming in several other ways, such as implementing its asset pipeline, testing framework, and configuration system.

Here are some specific examples of how metaprogramming is used in Rails:

  • When you create a new model, Rails uses metaprogramming to generate a table in your database and to define methods for interacting with that table.

  • When you create a new route, Rails uses metaprogramming to generate a regular expression that matches that route and defines a controller action that will be executed when the route is matched.

  • When you use Active Record to query the database, Rails uses metaprogramming to generate SQL code based on your query criteria.

  • When you use Rails associations, Rails uses metaprogramming to generate the necessary SQL code to manage those relationships.

Drawbacks of Metaprogramming in Ruby

Metaprogramming in Ruby is powerful, but it is essential to be aware of its potential drawbacks. Some of the caveats of metaprogramming include:

Reduced readability: Metaprogramming can make code more difficult to read and understand. This is because metaprogramming code often uses complex and abstract concepts.

Increased complexity: Metaprogramming can also increase the complexity of code. This is because metaprogramming code often adds additional layers of abstraction to the codebase.

Performance overhead: Metaprogramming can also introduce a performance overhead. This is because metaprogramming code often requires additional runtime processing.

Potential for bugs: Metaprogramming code can be more difficult to debug than regular code. Metaprogramming code often interacts with the Ruby language in complex ways.

In addition to these general caveats, there are some specific caveats to be aware of when using metaprogramming in Ruby. For example, it is essential to be careful when using the method_missing method, as it can be used to implement unexpected behaviour.

Here are some tips for using metaprogramming effectively in Ruby:

  • Use metaprogramming sparingly. Only use metaprogramming when it is necessary to implement a specific feature or to solve a specific problem.

  • Document your metaprogramming code carefully. Explain what your code is doing and why it is necessary.

  • Test your metaprogramming code thoroughly. Ensure your metaprogramming code does not introduce any new bugs into your application.

  • Avoid using metaprogramming to implement complex logic. If you need to implement complex logic, consider using a more traditional approach, such as object-oriented programming.

By following these tips, you can minimize the drawbacks of metaprogramming and maximize its benefits.

Closing Remark

Could your team use some help with topics like this and others covered by ShakaCode's blog and open source? We specialize in optimizing Rails applications, especially those with advanced JavaScript frontends, like React. We can also help you optimize your CI processes with lower costs and faster, more reliable tests. Scraping web data and lowering infrastructure costs are two other areas of specialization. Feel free to reach out to ShakaCode's CEO, Justin Gordon, at justin@shakacode.com or schedule an appointment to discuss how ShakaCode can help your project!
Are you looking for a software development partner who can
develop modern, high-performance web apps and sites?
See what we've doneArrow right