Today I’m going to walk through examples of metaprogramming. Wait, don’t run away! Metaprogramming is one of the more misunderstood topics in development, however it’s actually quite easy to understand if you look past the confusing name.
What is Metaprogramming?
If you’re like me the first time I heard the term metaprogramming my eyes glazed over. But let’s take a look at what metaprogramming actually is and later on we’ll review some practical examples of metaprogramming.
Technically metaprogramming is code that writes code. However, before you get visions in your mind of robots writing code to take over the world, understand that it’s actually much less intense.
Metaprogramming allows programs to create methods on the fly instead of having to define them in the program itself.
Examples of Metaprogramming
A Basic Example
Let’s begin with a basic example. I’m going to start with an example from the Ruby on Rails framework. Imagine that you have a database table for
users that have columns such as
Obviously the Ruby on Rails framework doesn’t know what columns that you’re going to have for the
user database. Which may cause you to be surprised that this database query will actually work:
So how exactly does the method
find_by_email work when the method wasn’t actually defined by you or the framework? Is it some kind of dark magic?
Not exactly. The Rails framework dynamically creates methods based on the column names in your database. So essentially the method is never defined, instead it is generated on the fly by leveraging metaprogramming techniques.
Practical Examples of Metaprogramming
When I’m learning a new topic I like to start by walking through base case examples. With that in mind we’re going to follow that pattern in this guide.
All of the examples that we’re going to go through will be in the Ruby language. I chose Ruby because it has a flexible metaprogramming interface and it’s very expressive.
There are a number of ways to implement metaprogramming. And the method you choose should be dictated by what type of feature you need to build. A few of the key options are:
- Leveraging monkey patching.
- Defining methods at application runtime.
- Defining methods on the fly.
If number 2 and 3 sound similar it’s because they are. In this guide we’ll examine the subtle differences with examples.
Examples of Metaprogramming: Monkey Patching
Starting off the list for examples of metaprogramming is monkey patching. Yes, it’s a weird name, but it’s a powerful and dangerous tool.
Example of Monkey Patching
Monkey patching is an object oriented programming technique that allows developers to:
- Open up classes
- Add or alter built in behavior
Let’s take a look at this code example. In this file I’ve opened up the built in Ruby
String class. Inside of the class I’ve added a new method called
gibberish that simply returns a string of random numbers.
If I call the method on a string it no longer prints out the string that was entered and instead prints out the random set of string characters. This is monkey patching in action. It essentially lets you open up classes and alter behavior.
When Monkey Patching is a Bad Idea
I mentioned earlier that this can be a dangerous practice, let’s see why.
Ruby has a built in method in the
String class called
upcase that converts all of the characters to uppercase values.
What would happen if we messed with the
upcase method? If you guessed that it could cause some nasty and confusing errors, you’d be 100% correct.
Even if your monkey patched method didn’t cause an error, it could cause some negative side effects throughout a program if any other developers are relying on the method that you altered.
For that reason it’s important to proceed with caution when it comes to performing monkey patching. Typically I won’t override Ruby core methods.
When Monkey Patching is a Good Idea
So when is monkey patching a good idea? I’ve had a number of times where outside libraries were causing issues in a program that I was working on. By leveraging monkey patching I was able to fix the bugs, which didn’t cause any negative side effects in the program.
Examples of Metaprogramming: Defining Methods at Application Runtime
Next on the list for examples of metaprogramming is defining methods at application runtime. What exactly does that mean? Well, have you ever had a set of methods that were very similar?
Let’s imagine that we have a
Book class that prints out details about each genre of book that our application manages. Do you notice how similar the methods look? This is a really poor programming practice because it requires us to change each method whenever we have to update the output for a book’s genre details.
Wouldn’t it be nice if we could simply define the method one time and then have the methods for genres generated each time the program runs? Well thankfully we can do that in Ruby.
We can get rid of all of the manual method calls and simply list the genres in an array. From there we can loop through the list and call
define_method. This will generate the full list of methods, just like we had before. Except now whenever we have to change the
details functionality we will only have to make the changes in a single place.
This is a great tool for removing duplicate code in classes and I find it helpful in many applications that I work with.
Examples of Metaprogramming: Defining Methods on the Fly
Last on the list of metaprograming implementations is to define methods on the fly. This option is one of the most powerful, but also can be confusing, so let’s walk through it as practically as possible.
Continuing with our book example, let’s imagine that you need to create a feature for searching through your list of authors. But instead of manually querying author details you want to have custom methods available to use. This type of design pattern is used frequently when building code libraries such as Ruby gems.
Method Missing Example
So for example, we want methods such as
author_genre to print out the author’s genre. However the tricky part is that our library may not know what the database column names will be. It could be
genre, or it could be
With this in mind we can’t use
define_method because we don’t know the exact names to put them in a list. This is where
method_missing comes in. Whenever Ruby comes across a method that isn’t defined in a program it looks to see if the developer implemented the
Essentially you can use
method_missing to let the program know of any types of methods you want to be generated on the fly.
In this program I’m using the Open Struct library to mimic creating an
Authors database. In a real world program we’d use a database, but the functionality, from a metaprogramming implementation perspective, is the same.
How does method_missing work?
The code explanation is a little out of the scope of this guide, if you want a deep dive into a
method_missing tutorial I have a full section dedicated to it in my Comprehensive Ruby Programming course.
At a high level we’re defining the
method_missing method. Which is something that Ruby looks for whenever it encounters a method that isn’t explicitly defined in the class.
From there we’re passing in the name of the attribute that we’re looking for, such as
first_name. From that point we tell
method_missing to pass that information to our mocked database to retrieve the attribute we called.
This means that
author_genre will actually function like a method, even though it was never explicitly defined by us in the method, pretty cool right?
It’s also important to implement
respond_to_missing? since it is a tool that many developers use to ensure that a method has been made available via metaprogramming.
How is this Different than Using define_method?
It may seem like
define_method are pretty similar, and they are. The key to remember is that
define_method generates the methods when the application starts and is typically based on a list of values.
method_missing is more dynamic and is checked whenever the method is called. This gives it more flexibility and is considered one of the most critical aspects to implementing metaprogramming in Ruby.
Drawbacks to Metaprogramming
On an important note, you should make sure to use metaprogramming with caution. Through the years I’ve witnessed many metaprogramming implementations and I’ve seen where they have unnecessarily over complicated a code base.
As you may have noticed, processes such as
method_missing are more challenging to read compared with normal Ruby code. This means that programs filled with metaprogramming modules can make it difficult to work with for new developers.
So make sure you consider the long term implications of your metaprogramming implementations. Just because you’ve created a metaprogramming component that works doesn’t mean that you should include it in your codebase. Like any other piece of your program, metaprogramming features should be easy to understand and should be modular so they can be changed in the future.
Remember the #1 rule of development: program requirements always change.
Summary of Metaprogramming
I hope that this has been a helpful list of examples of metaprogramming and that you have a good idea of how you can effectively utilize metaprogramming in your next project.