Metaprogramming in Ruby

Where the magic happens

Adam Kowalczuk
6 min readMay 7, 2023

The Ruby language and its popular framework, Ruby on Rails, are often described as magic. For those of you with a background in functional programming — like myself — who have recently dipped your toes in the waters of Ruby and OOP, you might be a little curious as to how this magic actually happens.

Enter the world of metaprogramming.

What is Metaprogramming?

A meme of Andy from the popular television show Parks & Recreation, unsure about what meta actually means.
Fair enough, Andy.

Meta itself is a term that gets thrown around a lot these days. It basically means to show awareness of or reference one’s self. In the world of computer science, metaprogramming is “the ability of programs to treat other programs as their data.” Okay — what the heck does that mean? In other words, metaprogramming is the act of writing code that writes code.

Ruby utilizes metaprogramming in a variety of ways, from introspection and monkey patching, to the send and method_missing methods. For the sake of brevity, I’m only going to talk about one concept here: dynamically defining methods. When I started to learn about metaprogramming (yup, it was yesterday afternoon), I found this concept the easiest to get a handle on and a great entry point into understanding metaprogramming in Ruby.

Getter and Setter Methods

When we’re introduced to Ruby, one of the first things we learn about is getting and setting instance variables. The following example shows the classic way of defining these methods that lets us call them on an instance of the Person class and read its name, or set a new name:

class Person 

# Constructor that sets name
def initialize(name)
@name = name
end

# Classic 'getter' method for reading name
def name
@name
end

# Class 'setter' method for setting name
def name=(name)
@name = name
end
end

person = Person.new('Alice') # Create instance of Person class with name 'Alice'
puts person.name # => 'Alice'
person.name = 'Bob' # Change instance name to 'Bob'
puts person.name # => 'Bob'

Here we can see that simply calling the name method on the instance person will return the name of that instance (‘Alice’), and that assigning a different name is just a matter of calling its setter counterpart using the assignment operator to set the new name (‘Bob’).

Cool, hey?

But what if we had a bunch of instance variables (age, height, weight, etc.) that also needed getter and setter methods? Our class definition would balloon considerably. Luckily, Ruby comes with methods right out of the box that create methods for getting and setting, all while using less lines of code.

class Person

# Creates a method for reading (getting) the value of @name
attr_reader :name
# Creates a method for writing (setting) the value of @name
attr_writer :name

# Constructor that sets name
def initialize(name)
@name = name
end
end

person = Person.new('Alice') # Create instance of Person class with name 'Alice'
puts person.name # => 'Alice'
person.name = 'Bob' # Change instance name to 'Bob'
puts person.name # => 'Bob'

Whoa! Okay, maybe not whoa yet, but cool nonetheless. The attr_reader and attr_writer are methods of the Module class and predefined in Ruby. They are used like keywords here within our Person class that “take attribute names as their arguments and dynamically create methods with those names.” When called on the person instance, each method behaves the same way as before by getting and setting as need be. Not only does this help to keep our code DRY — we now have two lines rather than six — but it also gets at the heart of what metaprogramming is: writing code that writes code.

If that wasn’t impressive enough, there’s an even cleaner way to accomplish getting and setting instance variables in Ruby:

class Person
# Creates methods for reading and writing the value of @name
attr_accessor :name

# Constructor that sets name
def initialize(name)
@name = name
end
end

person = Person.new('Alice') # Create instance of Person class with name 'Alice'
puts person.name # => 'Alice'
person.name = 'Bob' # Change instance name to 'Bob'
puts person.name # => 'Bob'

Is it an appropriate time to whoa yet? The attr_accessor method creates both a getter and setter method, basically doing the job of attr_reader and attr_writer in one fell swoop. Using this method might not always be the right choice (do we want to be able to get and set a variable?), but as far as keeping our code DRY, it’s pretty slick.

The “define_method” Method

Hopefully you’re starting to get a hint of the magic that is metaprogramming in Ruby. Building off of the previous examples that used attr_reader, attr_writer, and attr_accessor to create methods for getting and setting values on instance variables, Ruby gives us another way of dynamically defining methods at runtime. This can be accomplished with the define_method method.

Let’s say we have a class called Robot, and we want to create several methods that when called on an instance will return a suggestion to perform a particular piece of housework. As we all know, robots love doing housework.

class Robot

def initialize(name)
@name = name
end

def perform_clean_house
"Okay, #{@name}, let's clean house!"
end

def perform_cook_dinner
"Okay, #{@name}, let's cook dinner!"
end

def perform_wash_dishes
"Okay, #{@name}, let's wash dishes!"
end
end

robot = Robot.new('Rufus')
puts robot.perform_clean_house # => "Okay, Rufus, let's clean house!"
puts robot.perform_cook_dinner # => "Okay, Rufus, let's cook dinner!"
puts robot.perform_wash_dishes # => "Okay, Rufus, let's wash dishes!"

So we’ve defined a bunch of methods that when called return a suggestion to our robot. The problem is that each method basically does the same thing. If we want to collaborate with our robot on more tasks , we’ll have to create a new method for each task, which will make our Robot class definition less and less DRY. To break out of this pattern, we can use the define_method method to dynamically create methods for us:

class Robot

def initialize(name)
@name = name
end

["clean_house", "cook_dinner", "wash_dishes"].each do |action|
define_method("perform_#{action}") do |hour|
"Okay, #{@name}, let's #{action.gsub('_', ' ')} at #{hour} o'clock!"
end
end
end

robot = Robot.new('Rufus')
puts robot.perform_clean_house(4) # => "Okay, Rufus, let's clean house at 4 o'clock!"
puts robot.perform_cook_dinner(6) # => "Okay, Rufus, let's cook dinner at 6 o'clock!"
puts robot.perform_wash_dishes(7) # => "Okay, Rufus, let's wash dishes at 7 o'clock!"

So what’s going on here? We’ve taken each action (clean_house, cook_dinner, wash_dishes) we want to perform with our robot and stored it in an array. We then use define_method to write a new method using the action we want to perform, as well as insert the action into the string we’re returning (we’ve also added an hour parameter to our dynamic method that lets us provide a time we’d like to perform each action). We call our method on the robot instance and it behaves just like it did in the previous example, but now we have only one method definition, rather than one per each action. This also gives us the potential to simply change the array elements that define_method draws from, allowing us to easily create new methods as we see fit. Neat!

Just the Beginning

I hope the above examples have provided you with an understandable introduction to metaprogramming in Ruby. By dynamically defining methods, we can keep our code DRY, and we can keep it flexible. With versatility like this, it’s no wonder that so many people enjoy programming with Ruby.

References

This short article was not created in a vacuum. Feel free to explore the references below to learn more about the topics discussed here and metaprogramming in general:

A Practical Guide to Metaprogramming

Metaprogramming in Ruby

The Ruby Programming Language

Ruby Getters and Setters Method

Dynamic Method Definition with Ruby’s .define_method

Adam Kowalczuk is a full-stack web developer currently based in Victoria, British Columbia.

--

--