Metaprogramming in Ruby: Beginner Level
Tags: ruby, rails, programming, tutorial
Published: 2022-12-30 22:22:37

_This post is the first in a series focused on the application of Ruby metaprogramming. If you’re just starting to learn about metaprogramming, this is a great place to get started. For those who already know the basics, stay tuned for future installments that will cover intermediate and advanced topics._

Metaprogramming is a phrase you’ve probably heard once or twice in your career. Some may have uttered it with reverence and others may have flown into an apoplectic fit of rage at the very mention of it. In this article, we’ll discuss the basics of Ruby metaprogramming so that you can decide for yourself how and when to use it.


## Ruby Metaprogramming Questions: What is it?

Generally speaking, metaprogramming is the art and science of **creating a piece of code that generates more code** for you. This can be useful if you have recurring logic that you want to DRY (i.e., Don't Repeat Yourself) out. Let’s look at an example:


```ruby
class Truck
 attr_accessor :is_used

 def initialize(is_used:)
   @is_used = is_used
 end

 def Truck_condition?
   @is_used ? 'used' : 'new'
 end
end

class Sedan
 attr_accessor :is_used

 def initialize(is_used:)
   @is_used = is_used
 end

 def Sedan_condition?
   @is_used ? 'used' : 'new'
 end
end
```

In the above code, we have two classes, `Truck` and `Sedan,` with very similar logic but slightly different method names: `Truck_condition` and `Sedan_condition` (we’re breaking a couple of Ruby naming conventions here for the sake of illustration). Instead of repeating ourselves, we could _programmatically_ generate these methods using Ruby metaprogramming.

But why wouldn’t we simply refactor these classes and get the same result by inheriting from a parent class? Well, that leads to our next question….


## Ruby Metaprogramming Questions: When is it Useful?

When you introduce metaprogramming into your code, you start to create complexity that may confuse other developers later on – especially if they didn’t work directly on that code. In most examples like the one given above, you can and should favor simple inheritance.

That being said, here are a handful of cases in which metaprogramming might come in handy:


* If you’re working with data that doesn’t easily map to a database (e.g., calling a method whose response varies with time)
* If you’re working on a well-aged Rails monolith and you’re worried that refactoring could break something critical in the application
* If you’re purposefully trying to obfuscate the underlying Ruby code for a specific use-case (more on that later)

Metaprogramming is very powerful, but the truth is that it’s often overkill for the task at hand. It’s like when the Ikea manual firmly instructs you to hand-tighten, but you decide to grab your drill driver: We all want to take the opportunity to play with our power tools, but we’ll probably just end up breaking the furniture.


## Ruby `define_method`

One of the most important methods in Ruby metaprogramming is `define_method`. Here’s a basic example:


```ruby
class VehicleClass
 def initialize(is_used:)
   @is_used = is_used
 end

 define_method('Truck_condition') do
   @is_used ? 'used' : 'new'
 end

 def is_used
   @is_used
 end
end
```

This would create a method `Truck_condition` on `VehicleClass` that would return “used” if `is_used == true` and “new” otherwise.


## Ruby `define_method` With Arguments

You can pass arguments to methods created via `define_method`. We’ll go over this in more detail in the upcoming example, but here’s an isolated code snippet:

```ruby
define_method('Truck_report') do |name|
  "Car is used. Report made for #{name}"
end
```

In this example, we create a method `Truck_report`. When we call that method, we can pass in a string `name` that is added to the response. So calling `Truck_report('Alex')` would generate the string, “Car is used. Report made for Alex”.


## Implementing `attr_reader` with `define_method`

You may have also noticed that we defined a getter method for the `is_used` value. It’s not common to see this kind of syntax because we could use <code>[attr_reader](https://ruby-doc.org/core-2.2.3/Module.html#method-i-attr_reader)</code> instead. As a practical example, let’s use <code>define_method</code> to create our own custom <code>attr_reader</code> so that we no longer need a getter method in this class.

```ruby
class Class
 def custom_attr_reader(*attrs)
   attrs.each do |attr|
     define_method(attr) do
       instance_variable_get("@#{attr}")
     end
   end
 end
end

class VehicleClass
 custom_attr_reader(:is_used)

 def initialize(is_used:)
   @is_used = is_used
 end

 define_method('Truck_condition') do
   @is_used ? 'used' : 'new'
 end
end
```


We define `custom_attr_reader` on `Class`, which all Ruby classes inherit from. Calling `custom_attr_reader(:is_used)` within `VehicleClass` creates an `@is_used` method. This means that `VehicleClass.is_used` is now available for all instances of `VehicleClass` without the need for a getter method, serving a similar function to `attr_reader`.


## Ruby Metaprogramming Example

With that out of the way, let’s go over a basic metaprogramming example using Ruby 3.1.0 and `ActiveSupport::Concern`.

In this example, we’re creating a `VehicleClass` that should have a variety of car-specific methods. These methods will be built programmatically with Ruby `define_method` and included in `VehicleClass`. To begin, let’s define a “builder” method that will build the “vehicle condition” methods we worked with above.

```ruby
module VehicleBuilder
 extend ActiveSupport::Concern

 included do
   def self.build_vehicle_methods(vehicle_type:, is_used:)
     condition_method_name = "#{vehicle_type}_condition"

     define_method(condition_method_name) do
       is_used ? 'used' : 'new'
     end
   end
 end
end

module VehicleHelper
 extend ActiveSupport::Concern

 include VehicleBuilder

 included do
   build_vehicle_methods(
     vehicle_type: 'Truck',
     is_used: true,
   )
   build_vehicle_methods(
     vehicle_type: 'Sedan',
     is_used: false,
   )
 end
end

class VehicleClass
 include VehicleHelper
end
```

The `build_vehicle_methods` class will accept `vehicle_type` and `is_used` arguments. The `vehicle_type` argument is used to define the name of the “vehicle condition” method, while `is_used`  determines the response from that method.

We then call `build_vehicle_methods` within `VehicleHelper`, including our vehicle types and used/new status as arguments. By including `VehicleHelper` within our `VehicleClass`, we end up with the same methods defined earlier in the article: `Truck_condition` and `Sedan_condition`. 

You can easily verify this with an Interactive Ruby session. Simply open your terminal and enter `irb`. On your first line, enter `require "active_support/concern"`, then copy/paste the above code into your window. Once you’ve done that, create a new instance of `VehicleClass` and verify that you can call `Truck_condition` and `Sedan_condition` on that instance.

This approach is obviously a more roundabout way of defining those methods. But one advantage here is that you can build out new methods with consistent naming conventions by simply adding a new `build_vehicle_methods` call to `VehicleHelper`. That would be helpful if you have or plan to have a large number of “vehicle” classes; rather than copy/pasting a bunch of classes like `Truck` and `Sedan`, you can create them all within `VehicleHelper` and have guaranteed consistency.

And that advantage is compounded as the number of methods and complexity of logic increases. We can demonstrate by adding a few new methods to our `VehicleBuilder`:


```ruby
module VehicleBuilder
 extend ActiveSupport::Concern

 included do
   def self.build_vehicle_methods(vehicle_type:, serial_number:, is_used:, damages:)
     report_method_name = "#{vehicle_type}_report"
     condition_method_name = "#{vehicle_type}_condition"

     define_method(condition_method_name) do
       is_used ? 'used' : 'new'
     end

     define_method(report_method_name) do |name|
       # The "condition" method for this vehicle isn't yet defined
       condition_attr = send(condition_method_name)

       "#{vehicle_type} (SN #{serial_number}) is #{condition_attr}. Report made for #{name}."
     end

     damages.each do |damage|
       damage_method_name = 
         "#{vehicle_type}_has_#{damage}_damage?"

       define_method(damage_method_name) do
         true
       end
     end
   end
 end
end

module VehicleHelper
 extend ActiveSupport::Concern

 include VehicleBuilder

 included do
   build_vehicle_methods(
     vehicle_type: 'Truck',
     serial_number: '123',
     is_used: true,
     damages: %w[windshield front_passenger_door]
   )
   build_vehicle_methods(
     vehicle_type: 'Sedan',
     serial_number: '456',
     is_used: false,
     damages: []
   )
 end
end

class VehicleClass
 include VehicleHelper
end
```

In the example above, we’re still generating the “condition” methods from above, but we’ve expanded the use of Ruby `define_method` to include (1) a “report” method and (2) multiple “damages” methods.

The “report” methods, `Truck_report` and `Sedan_report`, will generate report strings similar to those demonstrated earlier in the article. But that string includes the condition of the car – how do we get that condition if the condition method isn’t yet defined? 

Ruby provides a `send` method that can address this problem. We can call `Truck_condition` and `Sedan_condition` from within the report-related `define_method` blocks using `send(condition_method_name)`. While this particular example is a bit contrived, the ability to call as-of-yet-undefined methods is quite useful for Ruby metaprogramming.

Finally, the “damages” methods are created by looping over the `damages` array. In this example, that creates `Truck_has_front_passenger_door_damage?` and `Truck_has_windshield_damage?` methods that simply return `true`.


## Ruby Metaprogramming Questions: Who Actually Uses This?

Earlier, we briefly touched on the “what” and the “when” of Ruby metaprogramming, but we didn’t discuss the “who”. If this is such a niche strategy, who actually uses it?

For guidance, we can turn to an oft-cited quote from Tim Peters, a major Python-language contributor, regarding Python metaprogramming: “_[Metaclasses] are deeper magic than 99% of users should ever worry about_”.

Regardless of the language(s) you work in on a day-to-day basis, metaprogramming is probably not a tool you’ll need to reach for often. There is a notable exception though: metaprogramming is perfect for designing a domain-specific language.

A domain-specific language (DSL) has its own classes and methods that obfuscate the underlying language they’re built with, reducing complexity and focusing on providing tools to accomplish specific tasks. [Gradle](https://gradle.org/) is a good example of such a use-case; it takes advantage of [Groovy metaprogramming](https://groovy-lang.org/metaprogramming.html#_testing_ast_transformations) to deliver a product focused solely on build automation.

Expanding upon this idea, anyone building a framework will likely find metaprogramming helpful (or even essential). [Rails](https://rubyonrails.org/) is one such framework built using the metaprogramming capabilities provided by Ruby. This can be illustrated by the Rails `enum` implementation; when you define an enum, the framework provides a variety of helpers out-of-the-box:

```ruby
class Vehicle < ApplicationRecord
 enum :body, [ :truck, :sedan, :minivan, :suv, :delorean ], suffix: true
end
```

By defining body as an `enum`, we automatically gain access to boolean checks like `Vehicle.truck?` and status setters like `Vehicle.sedan!`. Providing the `suffix: true` config option can make these helpers more readable by appending the column name, yielding `Vehicle.truck_body?` instead of `Vehicle.truck?`.  Database scopes are also generated for our `enum`, allowing us to retrieve all truck-body `Vehicles` with `Vehicle.truck` (and if you’re using Rails 6+, `Vehicle.not_truck` will return all `Vehicles` that do not have truck bodies).

This is Ruby metaprogramming at its best: taking Ruby code and augmenting it with human-readable, intuitive helpers for interacting with your database.

If this article piqued your interest, keep an eye out for the next installment in this series. In our [intermediate level post](https://www.thegnar.com/blog/metaprogramming-in-ruby-intermediate-level), we’ll dive into some practical examples of Ruby metaprogramming for those of us not building the newest “blazingly fast” framework.

_Learn more about how The Gnar [builds Ruby on Rails applications](https://www.thegnar.com/software-development/ruby-on-rails-development)._