Metaprogramming in Ruby

How to add a benchmark macro to your classes

Ravic Poon
3 min readMay 26, 2021
Photo by Fotis Fotopoulos on Unsplash

Many developers often hear that metaprogramming is a dark and scary skill that only Ruby enthusiasts can truly leverage. But in fact, it is quite the opposite. Metaprogramming is one of the traits that make Ruby a lovely programming language. It allows us to evaluate and modify existing methods, catch methods that do not exist, define methods dynamically at runtime (Code that writes code. Crazy, right?), and more. Let me help you demystify this topic with some examples.

Here we have a simple class, Teletubbies, which has two instance methods: tinky and winky. The first one will print a “tinky”, and the latter will print a “winky” after a delay.

Now picture a use case where you want to benchmark the run time of these methods and achieve something like this:

> t=Teletubbies.new
> t.tinky
"tinky"
Ran for: 0ms
> t.winky(2)
"winky"
Ran for: 2ms

Of course, there are many solutions to that. For example, we can require ‘benchmark’, print out the result and call it a day, or compare the time elapsed between the beginning and the method's last statement.

Yikes! That doesn’t look good, but it does the job. Is there a better way to organize the benchmarking logic? The answer is yes; there are many ways to DRY this up.

For one, we can try to refactor the time-related logic into a yield statement and encapsulates it as a service object. But I have found a good way to leverage metaprogramming via macros in Ruby.

Here we have a class CustomMacros, which has a class method benchmark. The purpose of this method is to calculate the time it spent to run a method.

The original_method here is an UnboundMethod; the original instance method from the inherited (Teletubbies). We do this because we want to reuse the same method names (tinky and winky) for those dynamically generated instance methods. So that we can bake the time-related logic into them as a “template” so that we do not have to copy and paste those for each original method we want to benchmark.

Finally, we can extend Teletubbies with CustomMacros to use the benchmark method.

Voilà! We have a DRY up version of the benchmark method that is flexible to modify and easy to use. On top of that, the codebase of Teletubbies is a lot cleaner, with the profiling implementation encapsulated as a macro.

You may find the declaration of benchmark familiar, like the ones we often use in ActiveRecord models, such as has_one, has_many, belongs_to. In fact, they are also macros! Not so magical now, don’t they? They are just many methods that generate code on the fly and perhaps the de facto proofs of how practical metaprogramming can be.

I appreciate the time you have spent reading this piece. I hope this article sheds some light on this “darker, magical-er” side of Ruby. But, most importantly, to encourage my fellow Rubyist to try and leverage this skill in your projects.

Clap a few times if you liked this; leave a comment if there is any mistake, and I will try my best to rectify it.

--

--