What is Metaprogramming?
Metaprogramming is a technique in which code operates on code rather than on
data. It can be used to write programs that write code dynamically at run time.
MetaProgramming gives Ruby the ability to open and modify classes, create
methods on the fly and much more.
A few examples of metaprogramming in Ruby are:
Metaclasses
You must have read about singleton
methods in Ruby. Singleton
methods are
declared on a single instance of a class and they are only available on that
particular instance contrary to instance methods which are available for all
instance methods of a class. For example:
In the above example, bark
is declared on the dog
object, hence it is only
available to that particular instance of the Animal
class.
Objects cannot hold methods, only classes can. But sometimes it is required for
objects to have methods. This is where the metaclass
comes in. Ruby provides a
metaclass
for each object which contains all the singleton
methods of an
object.
A metaclass
is also referred by other names such as an eigenclass
or a
singleton class
. We will be using the term metaclass
for all intents and
purposes in this chapter.
Let us understand the concept of metaclass
with the help of an example.
Previous example could be re-written as:
The syntax is different but it effectively does the same thing. In the above
example, class << dog
is used to open up the metaclass
of dog object and
bark
is defined inside the dog's
metaclass. Therefore it becomes the
singleton method of dog
We know that everything in Ruby is an object. Even classes are objects since
they are instances of the Class
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 a Ruby class as shown
below:
Now, let us declare a class method.
Since classes are objects in Ruby, and a class method is defined on a single
instance of class, they can also be called as singleton methods of the class
they are defined on.
Let's check for all singleton methods for the Animal class using the
instance_methods
method. instance_methods
can be called for all Ruby classes
, it returns an array containing names of all instance methods of a class.
We can exclude the instance methods of ancestor classes by passing false
as an
argument.
From the above example it is clear that class methods are nothing but singleton
methods of a class which are present inside the class's metaclass.
Usage of send in Ruby
Ruby gives us a convenient way to call any method on an object using send
. It
is an instance method of the Object
class.
send
method accepts the name of the method to be called as its first argument
and the remaining arguments passed to it are passed as arguments to the method
that is being called.
Method name can be passed as a string or a symbol but symbols are preferred. If
a string is passed, it is converted to a symbol. Given below is an example
showing how send
is used:
In the above example, send
method is used to invoke the eats?
instance
method of Animal
class.
Whenever send
method is invoked on an object, it sends a message to that
object. What we need to understand here is that any method call in Ruby is
actually a message passed to that object.
In case of the send
method, the name of method and arguments are passed as
message to the calling object.
send
can also be used to invoke private
and protected
methods of a class.
Defining methods dynamically
Consider a situation where you have to define a series of methods which are very
similar in the sense that they have the same basic structure except for a
string.
Sure, you can declare a method and pass the string as an argument to this
method. But the issue with this approach is that, it is not very declarative. As
in, the method name will now have to be generic.
Let us consider an example to understand this. Suppose we need to declare two
methods that perform two different actions. Let these actions be eating and
walking. Declaring two different methods called eat
and walk
will not be
very DRY. Instead we can declare a method and pass the actions as an argument as
shown below:
But the method in the above example is not very declarative since it doesn't say
which action is being performed. Also,perform
is a very generic name which
doesn't tell us a lot about which action the method is supposed to be
performing.
Lets see how we can re-write this so that the code becomes more declarative
while adhering to the principles of DRY.
In the above example, we have used metaprogramming to define two methods called
eat
and walk
. If you notice, the code that we have written is also acting as
the data required for generating methods.
Let's break down what is happening. define_method
is a Ruby method defined
inside the Module
class. It is used to define instance methods on the receiver
dynamically. It accepts two arguments, first being the name of method and second
argument is a block which becomes the method body and parameters of the block
become parameters of the method.
In the example above, we have not used any receiver explicitly on which
define_method
is being called. Hence self
object becomes the receiver which
in this case is the Animal
class.
Note: You should refrain from using define_method
for method creation in a
real-world codebase as it increases complexity and reduces the readability of
code.
A developer who looks at such a code in future, may not be able to figure out
the context easily and often make the code unmaintainable.
Defining missing methods on the fly
One more important aspect of metaprogramming is method_missing
method. It is
invoked by Ruby when receiver object is sent a message it cannot handle.
In other words, when a method is called on an object, Ruby first looks for the
method inside the object's metaclass, then it goes into the object's class and
looks for it in instance methods.
If it doesn’t find the method there, it continues to search up the ancestors
chain. If Ruby still doesn’t find the method, it calls another method named
method_missing
.
Ruby invokes missing_method
with the name of the missing method as a symbol,
arguments passed to the method and a block.
method_missing
is an instance method of the Kernel
class which Object
class inherits from.
The method_missing
method is used in the official Rails codebase to implement
a few functionalities like displaying a custom message when a method is not
found and also for dynamically creating a method which doesn't exist.
Let's see how we can dynamically create a method that doesn't exist using
concepts we have learnt so far.
Let's break down the code in above example. Calling can_swim?
and swim?
methods that don’t exist will invoke method_missing
.
Inside method_missing
, we want to create a new method only when the method
name includes "can". Otherwise we want to call super which will in turn call the
method_missing
method in Kernel
module which will throw a NoMethodError
exception.
Macros in Rails
Macros are everywhere in Ruby. You cannot get very far into the Ruby language
without encountering a Macro. A few common examples of Macros in Ruby/Rails are
attr_accessor
, has_many
and belongs_to
.
The first time you encounter a declaration like has_many
, it looks like
something built in the Ruby language. But it is just Ruby code. Ruby makes
programming in this declarative style easier than you might think.
Macros in Ruby are class methods that generate instance methods. Let us try to
understand how macros
work by implementing our own version of the has_many
macro.
Suppose there is a User
model with a has_many
association for comments
as
shown in the following example:
has_many
declaration is a call to the has_many
class method. Upon invocation
has_many
dynamically generates methods for managing the association.
In this case, it should generate the comments
instance method inside User
model which would return the comments belonging to each user.
We will try to define a has_many
class method inside the User
model which
will dynamically generate the instance methods for associations passed to
has_many
as arguments.
But before we do so, it is important to understand that all Ruby classes are
executable code i.e. All the code inside a Ruby class is executed during the
process of defining a class. For example, if we define the following class:
Hence every piece of code inside a class definition is executed while defining
the class.
Now, let's define the has_many
class method inside User
class.
Let's break down the code in above example. We know that every line of code
inside a Ruby class is executable. Therefore, when has_many
method is invoked,
it calls define_method
on User
and generates instance methods inside User
class by the name of association passed to has_many
as an argument.
Recall that we have already discussed this technique of dynamic method creation
using define_method
earlier in this chapter.
This way, we can have as many has_many
associations as we want inside our
User
model. Since every has_many
declaration is a class method call, an
instance method will be created for each association.
Note that in the above example, the receiver of has_many
and define_method
method calls is self
object and in this case it is equal to the User
model.
We have skipped self
as it is redundant. Ruby implicitly adds self
as a
receiver if there is no explicit receiver for a method call.
For the sake of simplicity, we defined the has_many
method inside the User
object itself. Ideally, methods like has_many
and belongs_to
are defined
inside the ActiveRecord::Base
class so that they are available to all the
model classes which inherit from ActiveRecord::Base
class.
Now we know how a class method or macro is used to generate instance methods.
Another popular macro is the attr_accessor
macro which generates a getter and
setter instance method for each attribute passed to the attr_accessor
method
call.
Now you might have a good idea about what a macro is and what metaprogramming is
etc. The point of this chapter was to give you an idea about what's happening
underneath the hood.
There is nothing to commit in this chapter as it's an in-depth chapter that
doesn't have anything to do with our granite application.