The Dynamic Rubyist
Macro Programming Made Simple
When I first encountered macros in Ruby, I thought, this is simple enough to use, but it must require some kind of wizardry to actually write a macro of my own. Later, I came back to the subject with a determination to unravel its mysteries. It turns out, writing one isn’t that hard, but it requires an understanding of something that you might not use on a regular basis. The process of writing a macro hinges on the ability to dynamically define and call methods and variables. To demonstrate this, I’ll walk you through rebuilding a few macros that Rubyists use on a regular basis. But first, let’s talk about what a macro is.
What is a macro?
By its simplest definition, a macro is a piece of code that creates code. Take for example attr_reader
: at the class level, it allows you to pass in arguments as symbols, which represent the name of the attributes that an instance of that class might have. It then uses these arguments to generate methods that let you see the value of those attributes.
class Painting attr_reader :name, :painter def initialize(name, painter)
@name = name
@painter = painter
endendtrees = Painting.new("Happy Trees", "Bob Ross")trees.name # => Happy Trees
trees.painter # => Bob Ross
Now we know that this painting is named Happy Trees, and it was painted by Bob Ross, but how does this macro work?
Classes are executable code
To fully understand how a macro works, it’s important to understand that a class is executable code. That is, everything inside the class definition is run when the class is defined.
class Painting attr_reader :name, :painter def initialize(name, painter)
@name = name
@painter = painter
end puts "I'm inside the class"endputs "I'm outside of the class"
When we run this file, we’ll get:
I'm inside the class
I'm outside of the class
If we understand that code inside a class is executed when the class is defined, it isn’t too outlandish to say that macros are just methods, and when you put one, like attr_reader
, at the top of your class definition, all you’re doing is calling the method, and passing it arguments. Now I bet we can build our own version.
Rebuilding attr_reader
To start off, let’s replace our call to attr_reader.
class Painting my_attr_reader :name, :painter def initialize(name, painter)
@name = name
@painter = painter
endend
Well, we’ve broken our code, but sometimes this is a perfect place to start. Let’s fix it by defining a method called my_attr_reader.
def self.my_attr_readerend
The self.
makes it a class method, and that’s what we want. As a class method it can be called on the entire class rather than just one instance. You might be wondering why we don’t use self as the receiver when we call it. Well, technically, we are. In Ruby, when a receiver is not explicitly given, self is used implicitly.
class Painting
self.my_attr_reader :name, :painter--- my_attr_reader :name, :painter
Thew two method calls work exactly the same.
Ok, we have a method, but it doesn’t do anything yet. We know we need to be able to pass it arguments, and we know there needs to be no limit to the number we can pass. We can do that by using *
.
def self.my_attr_reader(*attrs)end
We can now pass several arguments to .my_attr_reader
in the form of symbols that represent each attribute that our paintings can have. We probably want to do something with each attribute, so let’s set up some iteration.
def self.my_attr_reader(*attrs)
attrs.each do |attr|
end
end
This is where I introduce you to our first dynamic method. #define_method
is a built in Ruby method that takes in an argument of a string or symbol and dynamically defines a new method with that name.
def self.my_attr_reader(*attrs)
attrs.each do |attr|
define_method(attr) do end
end
end
When we call my_attr_reader :name, :painter
, we’re going to generate two new methods: #name
, and #painter
. #define_method
always creates instance level methods, but we want those methods to do something. We need to return the value of the corresponding instance variables when each method is called. To do that, I’ll introduce our second built-in dynamic method. #instance_variable_get
takes in a string or symbol as an argument and returns the value of the variable of that name. With a little interpolation, we can dynamically name the instance variable we want.
def self.my_attr_reader(*attrs)
attrs.each do |attr|
define_method(attr) do
instance_variable_get("@#{attr}")
end
end
end
We’ve done it! We’ve used one method to create two methods that can be called on any instance of our Painting class to return the value of an attribute.
class Painting def self.my_attr_reader(*attrs)
attrs.each do |attr|
define_method(attr) do
instance_variable_get("@#{attr}")
end
end
end my_attr_reader :name, :painter def initialize(name, painter)
@name = name
@painter = painter
endendtrees = Painting.new("Happy Trees", "Bob Ross")trees.name # => Happy Trees
trees.painter # => Bob Ross
Rebuild attr_writer
Where the attr_reader
macro generates methods to get the value of attributes, attr_writer
generates methods to set the values. To rewrite this one, we’re going to have to set variables dynamically. We can do this with the Ruby function #instance_variable_set
, which takes in two arguments: a string which represents the name of the variable, and the value to set the variable to. Our my_attr_writer
is going to look very similar to our my_attr_reader
.
def self.my_attr_writer(*attrs)
attrs.each do |attr|
define_method("#{attr}=") do |value|
instance_variable_set("@#{attr}", value)
end
end
end
Let’s look at the differences. The name of the generated methods are dynamically assigned as "#{attr}="
, to add an =
to the name making it a setter method. Then we tell it the argument by adding |value|
. This is the same as defining a method like def name=(value)
. Last we change instance_variable_get
to instance_variable_set
and use it to set the value of the instance variable.
Rebuild before_save and save
before_save
is a macro-style activerecord lifecycle callback method that registers a method to run before an object is saved to the database, and we’re going to rewrite it, as well as our own simplified version of activerecord’s save method. To start off, let’s give our Painting
class the ability to save objects to memory.
class Painting attr_reader :name, :painter @@all = [] def self.all
@@all
end def initialize(name, painter)
@name = name
@painter = painter
end def save
@@all << self
endend
Now let’s add a private method that we would like to run before saving a painting…
privatedef report
puts "saving #{self.name}"
end
…and we’ll register this method by calling my_before_save
.
my_before_save :report
Ok, but now we need to define .my_before_save
.
def self.my_before_save(*methods)
@@before_save_methods = methods
end
Here we’ve taken in an array of method names as arguments, and assigned them to the class variable @@before_save_methods
, but we still have to run those methods. To do that we need a way to dynamically call them. We can do that with my personal favorite built-in Ruby method, #send
. Let’s add that to #save
.
def save
if @@before_save_methods
@@before_save_methods.each do |method|
self.send(method)
end
end @@all << self
end
The code we added says if there are any @@before_save_methods
, for each of them, call the method with that name on self
, which here is the instance that #save
was called on. When we run trees.save
, #report
will be called and saving Happy Trees
will be printed to the terminal.
Summary
Hopefully what you take from this that writing macros in Ruby can be pretty simple as long as you know these powerful built-in functions for dynamically defining and calling methods and variables.
- #define_method: dynamically defines a new method
- #send: dynamically calls a method
- #instance_variable_set: dynamically defines and/or sets an instance variable
- #instance_variable_get: dynamically gets the value of an instance variable
Thanks for reading, and happy macro coding!