- Read Tutorial
- Watch Guide Video
This will be an interesting lesson, we are going to learn how to create code that will write code inside a Ruby program by leveraging the metaprogramming construct of method_missing
. Though it's an advanced Ruby concept, it's fun to learn and I think it'll be handy for you. So, let's get started.
Imagine we have a database called Author
and in it, we have different attributes such as first_name
, last_name
and genre
. Now, what do we do if we want to create dynamic methods on the fly? For example, if we create a new author in the database, we want custom methods to print out each of the attributes. At the same time, we don't want to hard code a bunch of methods, we want them to get generated dynamically. Essentially, we want a metaprogramming method that will write code dynamically on the fly based on the arguments sent to it.
Let's see how to do that.
require 'ostruct' class Author attr_accessor :first_name, :last_name, :genre def author OpenStruct.new(first_name: first_name, last_name: last_name, genre: genre) end def method_missing(method_name, *arguments, &block) if method_name.to_s =~ /author_(.*)/ author.send($1, *arguments, &block) else super end end end author = Author.new author.first_name = "Cal" author.last_name = "Newpot" author.genre = "Computer Science"
Let's walk through the code line by line.
In the first line, we are including a library that'll give us access to some methods. In this code, we'll be using it as a data structure to mimic a database, since creating a database and populating it would take up too much time.
Next, we are creating a class called Author
and we are defining attributes for it. In the next line, we are creating a method called author
, and inside it we are creating an OpenStruct
object with values for each attribute. Essentially this will function like a database.
Next, we are using a key method called method_missing
that is built into Ruby. In this sense, we are not creating a new method, but we are overriding an existing method. It takes three arguments, namely:
- A method name
- An array of arguments
- A code block
Inside this method, we are placing a conditional that checks if the method name that was passed starts with author_
, and if it does, we are calling the author
method and sending arguments to it. The argument $1
grabs the first element in the argument array, *arguments
will pass the remaining arguments, and &block
will pass the block of code.
If this conditional fails, we are calling super
, as we want the code to just call the parent class. Since we haven't inherited from any other class, Ruby will look for this method in BasicObject class, and will do nothing with it.
Why do we need to call super
?
Let's say we have a method that does not start with author_
. In that case, we are telling Ruby that we don't have any code for such a method. In other words, we want Ruby to generate methods only for those values that start with author_
, and not for other methods.
To check if this code works, let's instantiate Author
. We are creating a new variable called author
and we are setting attributes for this object.
Finally, let's test it out. Before that, let's print out our author's first name to know if the program is working fine. So, we add this piece of code to establish a base case:
p author.first_name
The output is:
Cal
and that's right!
Now, let's see what happens if we change it to
p author.author_genre
The output is Computer Science
.
So, we have successfully implemented a full meta-programming module for our Author
class.
If you see, we don't have a method called author_genre
, yet it returned the right value because this method was created on the fly by method_missing
. This is an important method to know as it can give you quite a bit of flexibility while building out Ruby programs.
One important thing I want to show you is the respond_to?
method. This method comes by default with all functioning methods, and it checks if a particular method exists in the code.
For example:
p author.respond_to?(:inspect)
will return the value true
because inspect
comes by default for all objects. On the other hand, if we say:
p author.respond_to?(:author_genre)
It returns the value false
because this method is not present in the code.
This is a potential problem, because many programmers will put respond_to?
in a conditional to check if a particular method exists before executing the remaining code, just to ensure that the program works. In the next guide, we'll see how to overcome this drawback.