Ruby Macros Explained

A Micro Look

Ellis Andrews
codeburst

--

It’s hard to get very far into the ruby programming language without encountering a macro. As soon as you start writing your first class, it becomes readily apparent that you’re going to need to be able manipulate instance attributes. And you’re a lazy programmer, so you’re definitely not about to write a both reader and writer method for each attribute. A quick google search later, and you’re here. Great! You write the single line of code adding the attr_accessor macro for the attributes in your class definition, and you carry on with your application.

This was me. I was willing to accept whatever sorcery was going on here, because it unblocked my progress. But then, like most rubyists, I began learning Rails.

More macros! Specifically, they’re heavily used in ActiveRecord, which comprises the “Model” layer of the “Model-View-Controller” framework around which Rails is designed. For example, models that inherit from ActiveRecord’s Base class have access to macros such as has_many, belongs_to, has_one, etc. which succinctly establish relationships between your models.

Ok, enough magic. I decided to figure out what was really going on here. Spoiler alert: you won’t need a wand.

Example: Soccer

I’ll use the domain of soccer teams and players to demystify the “magic” of macros. In this example, a Team has many Players, and a Player belongs to a single Team. For more background on model domains, check out my post on the topic.

Let’s start out with some basic models for soccer teams and players:

Model: Team
Model: Player

And let’s create some instances to play with:

Instance Setup

Neat! But what can we actually do with those classes right now? Answer: Not much.

We can neither read nor write the player’s attributes. In the code above, you can see in the error messages that we’re really attempting to call instance methods named Player#number and Player#number=, respectively, that don’t exist. Ah, that’s right, ruby doesn’t let you access those instance attributes directly. We need to do so through methods!

At this point, we can either:

  1. Add the builtin attr_reader and attr_writer macros (or the joint attr_accessor macro) to the models, and let these generate the methods for us.
  2. Write the methods ourselves.

Wouldn’t be a very helpful blog post if I left you at option 1, so let’s start with option 2.

Below I’ve defined the two missing methods on the Player class explicitly:

I’ve intentionally used the instance_variable_get and instance_variable_set methods above instead of accessing the instance variables directly with the @ sigil for reasons that will become clear later.

Hey, now our previous code works!

But our Player model has three attributes, and we’ve only implemented reader and writer methods for one of them. Maybe it wouldn’t be that bad to write these methods for the other two, but what if a Player had many attributes? We wouldn’t want to go through the process of writing both a getter (reader) and a setter (writer) method for every single attribute! Ideally we want the instance methods we just wrote to be created dynamically for us for each attribute. Enter: macros.

Definition: A “macro” is a class method that creates new instance methods.

Macros (in this context) are just ruby class methods that generate some code. Nothing “magical” about that!

So how do they work? To understand this, we first need to grok class definitions in ruby. The most important thing to note is that when a class is defined in ruby, that code is executed. Below is an example class definition.

If I were to run the file above, the output would be:

Hello from inside the Example class definition!
Calling class method from within the class definition...
Called: Example.example_class_method

We can use this to our advantage! I’ve tweaked our Example class slightly below to demonstrate:

Changes:

  1. Removed print statements showing when code was being executed.
  2. Added an initialize method so that we can set an instance attribute upon initialization to be read later.
  3. Renamed the class method from .example_class_method to .attr_getter, and let that method take in a single argument attribute.
  4. Changed the print statement to a function call within the class method.
  5. Removed the redundant self. from the beginning of the call to the class method, and passed in the symbol :foo as the now-required argument.

Sounds like a lot, but the only major change to the code was in #4. Within the class method, we are now calling the builtin define_method function. This is the crucial piece that allows us to dynamically define instance methods! That code is saying:

  1. Create a method named {{ value of attribute arg }} on the instance from which it is called.
  2. That method will call the instance_variable_get function on the string: "@{{ value of `attribute` arg }}".

Does that second part look familiar? It’s doing the same thing as the manual getter method we wrote initially for the Player’s @number attribute! With this set up, when we call .attr_getter with an instance attribute’s name as an argument, it will automatically create an instance reader method for us for that attribute. Additionally, we can call .attr_getter later on in the class in which it’s defined!

🧠 → 🥨

It’s a little confusing at first, but it’s all just plain old ruby! And we’ve managed to (mostly) recreate the behavior of the builtin attr_reader macro in our .attr_getter method!

If you’re still with me, let’s tidy our code up and round out the soccer analogy. First, let’s change our .attr_getter method so that:

  • It can accept an indefinite number of attributes for which to create methods.
  • It creates both a reader and a writer method for each attribute (a la attr_accessor).
  • It can be added to any class, instead of defined in one (i.e. put in a module).

Next, let’s create a naive version of the has_many ActiveRecord macro, so that we can easily specify that a Team has many Players.

And finally, let’s load the methods in our macro modules into a base class as class methods.

Now, when our model classes inherit from this CustomMacroBase class, the macro class methods will already be defined for us! All we need to do is call them with the appropriate arguments in our model classes and… voila! We have the instance methods we wanted.

If we use the instances from object creation file from above, we can test that the macro-created methods are working:

Output:

Steven Gerrard's Original Number: 17
Changing number to 8...
Steven Gerrard's New Number: 8
Original Manchester United Players: ["Wayne Rooney", "Ryan Giggs", "David Beckham"]
Original Liverpool FC Players: ["Steven Gerrard", "Michael Owen", "Fernando Torres"]
Michael Owen's Original Team: Liverpool FC
Changing team to Manchester United...
Michael Owen's New Team: Manchester United
New Manchester United Players: ["Wayne Rooney", "Ryan Giggs", "David Beckham", "Michael Owen"]
New Liverpool FC Players: ["Steven Gerrard", "Fernando Torres"]

I know reading code embedded in a blog post can be hard. I’ve formalized the code in this article into a lightweight repo for you to play around with. Happy coding!

--

--

Full stack developer working to demystify code through example. Mostly Python and React.