The Dynamic Rubyist: Macro Programming Made Simple

The Dynamic Rubyist: Macro Programming Made SimpleArthur TorresBlockedUnblockFollowFollowingFeb 5When 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 Treestrees.

painter # => Bob RossNow 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 codeTo 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 classI'm outside of the classIf 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_readerTo 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 endendWell, 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_readerendThe 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, :painterThew 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)endWe 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| endendThis 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 endendWhen 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 endendWe’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 Treestrees.

painter # => Bob RossRebuild attr_writerWhere 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 endendLet’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 savebefore_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 endendNow 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 :reportOk, but now we need to define .

my_before_save.

def self.

my_before_save(*methods) @@before_save_methods = methodsendHere 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 << selfendThe 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.

SummaryHopefully 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 variableThanks for reading, and happy macro coding!.. More details

Leave a Reply