Write Macros in Ruby
April 14, 2015
In Rails you use class-level declarations such as has_many
, belongs_to
, and other so-called “macros” all the time. For example:
class Movie < ActiveRecord::Base
has_many :reviews
end
class Project < ActiveRecord::Base
has_many :tasks
end
Look familiar?
Folks new to Rails (and Ruby) often assume these declarations are a magical aspect of Rails. In fact, there is no magic—it’s just Ruby code. Indeed, Ruby makes programming in this declarative style easier than you might think.
In this short video tutorial we recreate a simplified version of the has_many
declaration from scratch so you understand how to apply this same powerful technique in your own Ruby code!
Today we’re going to tap into the power of Ruby objects and methods to write a class-level declaration, sometimes called a “macro”. Here’s an example of what I’m talking about from Rails…
class Movie < ActiveRecord::Base
has_many :reviews
end
class Project < ActiveRecord::Base
has_many :tasks
end
The first time you encounter a declaration like has_many
, it looks like something built in to the Ruby language or some magical aspect of Rails. But in fact, it’s simply Ruby code. Ruby itself makes programming in this declarative style easier than you might think. And once you understand how it works, you’ll be more confident with Rails and be able to use this same powerful technique in your own Ruby code.
So let’s create a simplified version of this code from scratch, building up from the underlying principles that make it work.
Singleton Methods On An Object
Here’s a simple String
object, and we call the upcase
method which is defined in the String
class:
dog1 = "Rosco"
puts dog1.upcase # => ROSCO
And no surprise, it prints out the dog’s name upcased: “ROSCO”.
So we can call any method in the String
class, but Ruby also lets us define methods on a specific object. For example, here we define a hunt
method on the dog1
object:
def dog1.hunt
puts "WOOF!"
end
dog1.hunt # => WOOF!
And when we call the hunt
method on the dog1
object, it prints “WOOF!”.
OK, now here’s a different String
object:
dog2 = "Snoopy"
Now what happens if we try to call the hunt
object on the dog2
object?
dog2.hunt # => undefined method `hunt`
Well, this dog don’t hunt! The hunt
method is only defined on the dog1
object. You’ll often hear this referred to as a singleton method because the hunt
method is only defined on a single object: the dog1
object in this case.
That’s interesting, but when would you ever want to do this? Turns out, you use singleton methods all the time in Ruby! And we need one to define our has_many
declaration.
Classes Are Objects, Too!
Here’s a simple Movie
class:
class Movie
end
In Ruby, classes are objects, too. In fact, we can print out the class of the Movie
class:
p Movie.class # => Class
The class of the Movie
class is Class
. And that in itself is an object, which we can see by printing out the object’s id:
p Movie.class.object_id # => 70233956488680
Interestingly to note, Movie
is a constant that references the Class
object.
So classes in Ruby are objects, too. And any given Ruby class is an object of class Class
.
Singleton Methods On A Class
So if a Ruby class is an object in its own right, we can treat it like any other object. For example, we can define a singleton method on the Class
object referenced by the Movie
constant:
movie_class = Movie
def movie_class.my_class_method
puts "Running class method..."
end
my_class_method
is just a singleton method defined on the Movie
class object. To call that method, the receiver of the call is the Movie
class object.
movie_class.my_class_method # => "Running class method..."
So that’s basically the same thing we did with the dog1
object earlier. In this case we defined the singleton method on the Class
object.
To tidy up this code, we don’t need the temporary movie_class
variable since it’s just a reference to the Movie
class object. Here’s a version of the code that doesn’t use the temporary variable:
def Movie.my_class_method
puts "Running class method..."
end
Movie.my_class_method # => "Running class method..."
And that works just as well!
To make this look more traditional, we can move the method inside the class declaration:
class Movie
def Movie.my_class_method
puts "Running class method..."
end
end
Movie.my_class_method
And that also works!
Here’s the take-away: In Ruby, there is no such thing as a “class method”. my_class_method
is just a singleton method defined on the Movie
class object.
So that’s the first principle, but it doesn’t look like the has_many
declaration quite yet.
Classes Are Executable Code
The second principle is that class definitions are executable code. We can prove that by adding some print statements:
puts "Before class definition"
class Movie
puts "Inside class definition"
def Movie.my_class_method
puts "Running class method..."
end
end
puts "After class definition"
Movie.my_class_method
And when we run it, we get:
Before class definition
Inside class definition
After class definition
Running class method...
So code is executed during the process of defining the class. And if that’s the case, then we can actually run the my_class_method
method inside the Movie
class definition:
puts "Before class definition"
class Movie
puts "Inside class definition"
def Movie.my_class_method
puts "Running class method..."
end
Movie.my_class_method # run the method as the class is being defined
end
puts "After class definition"
If we run it now, we get:
Before class definition
Inside class definition
Running class method...
After class definition
So the my_class_method
method is called as the class is being defined!
But using the Movie
constant seems repetitive. Turns out, during the class definition, Ruby sets the self
variable to the class object being defined. Let’s print out the value of self
as the class is being defined:
puts "Inside class definition of #{self}"
If we run that, we get:
Before class definition
Inside class definition of Movie
Running class method...
After class definition
So this proves that self
is set to the current class object being defined, which is the Movie
class object in this case.
That being the case, we can replace Movie
with self
:
def self.my_class_method
puts "Running class method..."
end
self.my_class_method
And that works just the same!
Now this self.my_class_method
line is starting to look like the has_many
class declaration that we want, except for the self.
part. In this context, self
is the receiver of the my_class_method
method call. Buf if there’s no explicit receiver, Ruby implicitly uses self
as the receiver.
So we can remove the self.
part:
my_class_method
And that still works!
Renaming
So this is looking closer to the style of declaration we want. Let’s clean it up by removing the spurious puts
calls and renaming the method to has_many
so it looks more familiar:
class Movie
def self.has_many(name)
puts "#{self} has many #{name}"
end
has_many :reviews
end
Notice the has_many
takes a name
argument, so calling has_many :reviews
passes :reviews
as the argument.
If we run it, we get:
Movie has many reviews
Notice the value of self
is the Movie
class object.
Define Method
OK, so the has_many
method is being called during the class definition, so now what should the method do? Well, in Rails has_many
dynamically generates a handful of methods for managing the association.
For example, in this case it would generate a reviews
method that returns the reviews associated with the movie. And we’d call it like so:
movie = Movie.new
movie.reviews # => undefined method
And if this were Rails, calling movie.reviews
would return an array of reviews associated with that movie.
But if we try to run it, we get an “undefined method” error because there is no reviews
instance method defined on our movie
object. But we can define that method!
It would look something like this:
def self.has_many(name)
puts "#{self} has many #{name}"
def reviews
puts "SELECT * FROM reviews WHERE..."
puts "Returning reviews..."
[]
end
end
But hard-coding the method name won’t work if we have another association, for example a genres
association:
has_many :reviews
has_many :genres
To make this work, we need to dynamically define a method for each association: in this case a method named reviews
and a method named genres
. And we don’t know the names of those methods until runtime—until the class is being defined. So we need to define those method dynamically, on the fly.
Let’s start with the reviews
method:
has_many :reviews
To do that, we can use define_method
:
def self.has_many(name)
puts "#{self} has many #{name}"
define_method(name) do
puts "SELECT * FROM #{name} WHERE..."
puts "Returning #{name}..."
[]
end
end
define_method
takes as its argument the name of the method to generate and the body of the block becomes the body of the method. define_method
always defines an instance method in the receiver, which in this case is the Movie
object. So we’ll end up with a reviews
instance method in the Movie
class.
Now when we run it, we see that the reviews
method got defined because we see the output of calling that method:
Movie has many reviews
SELECT * FROM reviews WHERE...
Returning reviews...
In fact, we can call reviews
multiple times:
movie.reviews
movie.reviews
And when we run it, we see that the method is defined once but run multiple times:
Movie has many reviews
SELECT * FROM reviews WHERE...
Returning reviews...
SELECT * FROM reviews WHERE...
Returning reviews...
And because we’re defining the has_many
method dynamically, we can now declare multiple associations:
class Movie < ActiveRecord::Base
has_many :reviews
has_many :genres
end
movie.reviews
movie.genres
Now two methods are dynamically defined as the class is being defined, and we can call those methods:
Movie has many reviews
Movie has many genres
SELECT * FROM reviews WHERE...
Returning reviews...
SELECT * FROM genres WHERE...
Returning genres...
So now our has_many
method is dynamically defining methods based on the name of the association!
This is pretty cool, but it currently only works for movies. We’d like to share the has_many
method across multiple classes. And we can do that using inheritance.
Class Method Inheritance
To share the has_many
method using inheritance, first we’ll define it in class named Base
inside a module named ActiveRecord
(just so it’s similar to Rails):
module ActiveRecord
class Base
def self.has_many(name)
puts "#{self} has many #{name}"
define_method(name) do
puts "SELECT * FROM #{name}..."
puts "Returning #{name}..."
end
end
end
end
Then the Movie
class can inherit from the ActiveRecord::Base
class:
class Movie < ActiveRecord::Base
has_many :reviews
has_many :genres
end
And everything works just as it did before:
Movie has many reviews
Movie has many genres
SELECT * FROM reviews WHERE...
Returning reviews...
SELECT * FROM genres WHERE...
Returning genres...
Notice that the value of self
is the Movie
class.
And now that we’re using inheritance, we can define a new subclass named Project
that has many tasks:
class Project < ActiveRecord::Base
has_many :tasks
end
project = Project.new
project.tasks
And running that gives us:
Project has many tasks
SELECT * FROM tasks WHERE...
Returning tasks...
Notice that the value of self
is the Project
class.
Summary
So now we’ve come full circle and implemented the two models we wanted with has_many
declarations:
class Movie < ActiveRecord::Base
has_many :reviews
has_many :genres
end
class Project < ActiveRecord::Base
has_many :tasks
end
Now, Rails does a whole bunch more stuff under the hood with these declarations, but hopefully that helps demystify these class-level declarations, sometimes called “macros”.
Just remember: there’s nothing special or magical about these methods. They’re just regular Ruby methods that generate code!
Learn Ruby Today!
If you're using Rails and it feels too magical or you struggle with Ruby concepts, check out our in-depth Ruby course. You'll learn how to design object-oriented programs the Ruby way, deepen your understanding of the Ruby programming language, and demystify the "magic" of Ruby on Rails!