The ruby programming language is known for two major facts: One is its core philosophy with object-oriented programming where "Everything is an object", the other important one is its incredible flexibility to define DSLs and "magic" classes. This magic is on purpose and a quite special feature of ruby called metaprogramming, in this article we'll see more about the deep nested details of ruby and how to create magic APIs with metaprogramming!
Table of contents
- What is metaprogramming anyway?
- In ruby everything is an object, what does that mean?
- But what about rails? How this framework applies that concept for maximum developer experience
- How to define methods dynamically
- Using hooks to detect moments on the instantiation of the class
- Conclusion
What is metaprogramming anyway?
Metaprogramming is a concept where you write a program to write a program, confusing, huh? But trust me it's quite simple, imagine the following scenario:
You create a function that write another file with a specific functionality, or even append content on the same file with the whole implementation for a class or an object or whatever, this is code creating more code got it? This is metaprogramming!
A cool example of metaprogramming are DSLs (Domain specific languages) that define a specific syntax within the same language to express far more complex concepts, such as querying a database or defining routes. It's not new that the ruby ecosystem uses a lot of metaprogramming, so we can keep writing elegant and clean code with that "magic" feel to it.
In Ruby everything is an object, what does that mean?
It's possible that you saw the famous phrase "In ruby, everything is an object", but what does that even mean and how it's related to metaprogramming at all?
In ruby all the things are objects under the hood, even the core data structures like numbers, strings, etc. as you can see on the example below:
irb(main):009> 5.class
=> Integer
irb(main):010> 5.class.class
=> Class
irb(main):011> ''.class
=> String
irb(main):012> ''.class.class
=> Class
irb(main):013> true.class
=> TrueClass
irb(main):014> true.class.class
=> Class
At the end of the day, even simple structures are defined from a common class object and can have all the things objects are known for such as methods, inheritance, etc...
This allows crazy manipulation possibilities, and it's on that specific field where metaprogramming lives and thrives, since all things are objects we gain the power of changing that object dynamically. Allowing us to create DSLs within the language easily.
Let's try to understand this metaprogramming thingy together without using any modern syntax provided by ruby, shall we?
Disclaimer: The following example are not how to actually implement metaprogramming, it's just an abstraction so you can understand the general idea around the concept.
Imagine we have a hash with a function inside, simulating a class object with method, we can even call this function by accessing the "property" and calling the "method".
my_obj = {
fn: ->() { puts 'original method' }
}
my_object[:fn].() # => original method
After defining this initial object, it's possible to write a function that receives this object and append another function to it as follows:
my_obj = {
fn: ->() { puts 'original method' }
}
def add_new_method(obj, cb)
{another_fn: cb}.merge(obj)
end
And with this function working, we can append new functionalities to it by calling the newly created function:
my_obj = {
fn: ->() { puts 'original method' }
}
def add_new_method(obj, key, cb)
{key => cb}.merge(obj)
end
my_obj[:fn].() # => original method
my_obj = add_new_method(my_obj, :another_fn, ->() { puts 'another method' })
my_obj[:another_fn].() # => another method
my_obj = add_new_method(my_obj, :hello, ->(target) { puts "hello #{target}" })
my_obj[:hello].("world") # => hello world
This is the core concept of metaprogramming, of course that modern functions provided by ruby itself lets you do far more things such as listen to specific events etc. But the core mindset around it is manipulating the underlying objects and adding new functions to it.
But what about rails? How this framework applies that concept for maximum developer experience
The Ruby on Rails framework is the most known and powerful ruby gem for a long time, and its core philosophy evolves around providing the smallest bit of elegant code to achieve a lot of features on your application. To provide that level of abstraction and elegant syntax, rails rely a lot on metaprogramming, so we can write less and achieve more on our codebase.
Methods like validates
, has_many
, scope
, before_save
are just a couple that comes with the ORM part that provide various benefit for declarative model code as shown on the examples below:
class User < ApplicationRecord
has_many :posts
validates :username, presence: true
before_save :hash_password
scope :recent, -> { where('created_at > ?', 1.week.ago)}
private
def hash_password
self.password = hash(self.password)
end
end
-
has_many
enable us to express a database relationship of users and posts within the model itself, in this particular example thePost
model should use another method calledbelongs_to
. -
validates
allow validation when we try to perform any creation method using this particular model -
before_save
act as a hook informing a method as a symbol, this particular method will be called before the record is saved on the database. -
scope
provide a way to create a new method on this model with the particular query, that way we can call something likeUser.recent
and get the result of the function.
And there is much more metaprogramming patterns on different parts of the framework such as controllers, views, helpers, etc. It's truly an important part of what makes rails a beloved framework.
How to define methods dynamically
Now for the more practical parts, creating methods dynamically is as easy and as elegant as anything in ruby! Hopefully we're using a well though language, right? 👀 /j
The most basic action is defining a method dynamic, and we can use the built-in syntax provided by ruby called define_method
, below we'll see a practical example:
class FirstExample
define_method(:example_method) do
puts 'example method'
end
end
FirstExample.new.example_method # => example method
class SecondExample
def self.define_new_method(method_name, &body)
define_method(method_name, &body)
end
end
SecondExample.define_new_method(:hello) { |target| puts "Hello #{target}" }
SecondExample.new.hello("world") # => Hello world
The FirstExample
class show how a simple usage of the define_method
looks like, it accepts a symbol as the first parameter and a block that can optionally receive an argument and perform any actions (it'll be replaced as the class method once it's instantiated), at the end of the example we can simply instantiate the class with .new
and call the dynamically defined method.
The SecondExample
is more involved and define a method to define methods(finally some meta-language references), although it's simply an abstraction on top of the define_method
function itself we can see the power that this can lead to a real codebase right? An important detail is that we accept the second parameter as &body
because this tells ruby to accept as a block that we'll latter use on the example with the hello
method.
Using hooks to detect moments on the instantiation of the class
Ruby not only provide a way to create new methods dynamically, but also provide hooks, so we can run arbitrary code when a specific action happen (around the object cycle of course).
Things such as running some code when a class is inherited or included and even when a particular method does not exist within a class, pretty cool and powerful, huh?
We'll see in detail about some possibilities with different hooks below:
method_missing
This hook function allow the run of arbitrary code once an unknown method is called within a class, below it's an example of a class implementing it:
class MethodMissingExample
def method_missing(name, *args)
puts "An unknown method was called using the name -> #{name} and the arguments -> #{args.inspect}"
end
end
MethodMissingExample.new.test_method(1, 2, 3) # => An unknown method was called using the name -> test_method and the arguments -> [1, 2, 3]
As you can see, by simply creating a method with the name method_missing
we already enable this functionality and can act on it with the name
and the args
of the method at hand. This can be quite useful if you're working with public code that it's used by other developers and want to provide custom experience when a method is misspelled or indeed not implemented.
included
This method works only for module and will run a particular block when the module is included into another class or module as shown below:
module IncludedExample
def self.included(name)
puts "The #{name} was included!!"
end
end
module Test
include IncludedExample # => The Test was included!!
end
Similarly, we have a method for the extend
and prepend
action within modules, it works the same as the includes, and it's shown below with an example:
module ExtendedExample
def self.extended(name)
puts "The #{name} was extended!!"
end
end
module PrependedExample
def self.prepended(name)
puts "The #{name} was prepended!!"
end
end
module Test
extend ExtendedExample # => The Test was extended!!
prepend PrependedExample # => The Test was prepended!!
end
inherited
Back to the classes with a similar functionality, we can create a hook that will be run every time a class is inherited, as shown below:
class InheritedExample
def self.inherited(name)
puts "The #{name} class was inherited!!"
end
end
class Test < InheritedExample # => The Test class was inherited!!
end
Conclusion
Hope this was useful for anyone reading it! metaprogramming is a very fun subject to learn and use, it can create very messy code but with good architecture it's possible to create amazing experience such as with Ruby on Rails. Just reach out if I can help with anything and may the force be with you 🍒