Some days ago I’ve tried to explain the concept of Metaprogramming to a friend who is starting in the IT area and is studying Ruby as her first language, my influence by the way, but I wasn’t satisfied with what I managed to teach about this subject.
I follow this motto: “You really understand something when you are able to comfortably teach it”. So here I’m, teaching myself with this article.
With a quick web search I found the following:
Metaprogramming is a technique by which you can write code that writes code by itself dynamically at runtime. This means you can define methods and classes during runtime. Crazy, right? In a nutshell, using metaprogramming you can reopen and modify classes, catch methods that don’t exist and create them on the fly, create code that is DRY by avoiding repetitions, and more.
Source: https://www.toptal.com/ruby/ruby-metaprogramming-cooler-than-it-sounds#metaprogramming
So, in other words, it means that we use metaprogramming when we need to implement something dynamically, something that we can’t know exactly what will be, but we have a general notion of how it should behave.
When we need to check the current environment in Rails, we usually use
Rails.env.<environment_name>?
, something like this:
if Rails.env.production?
... # Do something in the Production environment.
end
What you may not know is that Rails.env
belongs to ActiveSupport::StringInquirer
class. This
class uses method_missing
, so that you can call env.<environment_name>?
instead of
env == "environment_name"
.
The part of code that does this metaprogramming in ActiveSupport::StringInquirer
looks like:
def method_missing(method_name, *arguments)
if method_name.end_with?("?")
self == method_name[0..-2]
else
super
end
end
You can see the full original code here.
These use of method_missing
here means that if a method that wasn’t implemented directly with
def ... end
, so a missing method, and its name ends with ?
then compare its name with the value
of the instance of ActiveSupport::StringInquirer
.
Another example that you can try is running the following code in some Rails application that you
have in its rails console
:
# Inside a Rails console
vehicle = ActiveSupport::StringInquirer.new('car') # => "car"
vehicle.car? # => true
vehicle.bike? # => false
vehicle.motorcycle? # => false
vehicle.dummy? # => false
As you can see, a multitude of methods are available for this vehicle
, which is an instance of
ActiveSupport::StringInquirer
, where only vehicle.car?
returns true
because its value is
"car"
.
method_missing
in RubyThis method is a private method in the BasicObject
, which is the parent class of all classes in
Ruby, it’s an explicit blank class. When some part of a code tries to execute a nonexistent method
of Class, method_missing
is called. The trick here is that you can override this method to
provide some dynamic behavior, like we saw in ActiveSupport::StringInquirer
.
In the official documentation, we find this explanation:
method_missing(symbol [, *args] ) → result
Invoked by Ruby when obj is sent a message it cannot handle.
symbol
is the symbol for the method called, andargs
are any arguments that were passed to it. By default, the interpreter raises an error when this method is called. However, it is possible to override the method to provide more dynamic behavior. If it is decided that a particular method should not be handled, thensuper
should be called, so that ancestors can pick up the missing method.
So as you can see, method_missing
was implemented in the BasicObject
not only to deal with
nonexistent methods, but also to be overridden in classes that we code. Here is an example with the
usage with super
:
class Dummy
def method_missing(method_name, *args)
name = method_name.to_s
super unless name.start_with? "set_"
attr_name = name.delete_prefix("set_")
instance_variable_set("@#{attr_name}", args[0])
end
end
dummy = Dummy.new
dummy.set_name("Eugenio")
dummy.instance_variable_get("@name")
# => "Eugenio"
dummy.set_hash({key: "vaule"})
dummy.instance_variable_get("@hash")
# => {:key=>"vaule"}
dummy.nonexistent_method # it will trown an error
# => undefined method `nonexistent_method' for #<Dummy:0x00007f5e2dac29d8 @name="Eugenio", @hash={:key=>"vaule"}> (NoMethodError)
In this example a bunch of methods is dynamic defined to set and to allocate local variables of an
instance of Dummy
, but if this method name doesn’t start with “set_” than the original behavior
of method_name
is called through super
.
define_method
in RubyBesides method_missing
, we also can use define_method
to do some metaprogramming. Both these
methods are the most used to do this.
define_method
is written in the Module
class, which is a collection of methods and constants.
Although method_missing
is defined in the BasicObject
, instead of this class Module
, the
usage of both are quite the same.
Note: If you read more about this Module
class you will probably see something like
“Module
is the superclass of Class
and is also an instance of Class
“. It is confusing, I
know, but the best explanation that I found is
this answer by John Douthat in the Stackoverflow.
In the official documentation,
we find this explanation of define_method
:
define_method(symbol, method) → symbol
define_method(symbol) { block } → symbol
Defines an instance method in the receiver. The method parameter can be a Proc, a Method or an UnboundMethod object. If a block is specified, it is used as the method body. If a block or the method parameter has parameters, they’re used as method parameters. This block is evaluated using
instance_eval
.
The meaning of “This block is evaluated using instance_eval
” is that a code with define_method
will be evaluated by a Ruby interpreter in the context where it is called, so if you call it inside
some class it will be treated as an instance method, like a def some_instance_method; end
, on the
other hand if you call it inside a class << self; end
it will be treated as a class method. Here
is a simple example:
class MyClass
define_method :some_instance_method do
"Hi, I'm a instance method --> self value: #{self}"
end
class << self
define_method :some_class_method do
"Hi, I'm a class method --> self value: #{self}"
end
end
end
MyClass.some_class_method
# => "Hi, I'm a class method --> self value: MyClass"
my_obj = MyClass.new
# => #<MyClass:0x00007f564213a818>
my_obj.some_instance_method
# => "Hi, I'm a instance method --> self value: #<MyClass:0x00007f564213a818>"
define_method
use.Imagine that you have a Post
class for some blog and this class has a status attribute that
can be draft, published or unpublished. To build some “status check” for each of these
possibilities this code was written:
class Post
# have a status attribute that can be draft, published or unpublished.
def draft?
status == "draft"
end
def published?
status == "published"
end
def unpublished?
status == "unpublished"
end
end
We definitely can do this better with define_method
:
class Post
define_method "#{method_name}?" do
status == method_name
end
end
With this approach we not only build the three methods for the three states of status attribute,
but also build the methods for possible future new states! If an edited status appears
besides the others, we already have the edited?
method. Also, if some status is no longer used
we don’t need to erase its check method. All methods are dynamically built!