Key Takeaways
- Metaprogramming in Ruby, though complex, can be a beneficial tool for developers, potentially creating less repetitive, more easily tested, and cleaner code. It involves any code that significantly raises the level of abstraction or any code that creates code.
- Class/Module hooks in Ruby are methods that get triggered when certain events occur, such as when a class inherits from another class or when a module is included in a class. These hooks include methods like #included, #extended, #inherited, and #prepended.
- The #included hook in Ruby is used to execute arbitrary code whenever a module is included into a class, often used to add instance methods to the including class or module. The #extended hook works similarly, but fires when the module is extended.
- Metaprogramming and hooks in Ruby can be used to create Domain Specific Languages (DSLs), add behavior to classes or modules dynamically, track inheritance or module inclusion, and create plugins or extensions. However, careful application is necessary as they can make code more complex and harder to debug if used improperly.
Any code that significantly raises the level of abstraction and/or any code that creates code. -MeThis definition is wildly oversimplified. But in order to start learning to metaprogram you have to have something you can point to and say “That is metaprogramming.” I’m also intentionally side-stepping the “There is no metaprogramming, there is only programming” stance here. While I partially agree, this is not the place for such a philosophical debate. If you are interested in that debate, there is a great Ruby Rogues podcast about it where several prominent “Rubyists” discuss the definition of metaprogramming in much greater detail.[1] To start with, metaprogramming is not magical. It will not solve all of your problems. Its usage will not make you a rock star. I’m also going to bypass the whole, “You really shouldn’t run with scissors,” warning and assume that you’ll apply sound logic in your application of metaprogramming. Caveats aside, it can be a wonderful addition to your toolbelt. Quite simply, it is a way to get your code to do a little bit more for you. If used properly, it will result in less repetitive, more easily tested, and cleaner code. It may be a little confusing to understand at first, but once you get the knack of it you can solve a host of problems that would be incredibly difficult otherwise. In Part One of the guide, I’m going to run through several class/module hooks. These hooks will allow you to improve the readability of your code. Sometimes it feels like magic, but it’s not. If you get lost anywhere, just remember rule #1. Also, look to the end of the post for links to more resources/help.
The hooks
inherited
The first hook I’d like to discuss is#inherited
. It is the hook that allows code like this to exist in Rails:
[gist id="2470567"]
The new developer sees this and either ignores it or infers a comment above class Posts
that says something to the effect of #there be dragons here
. Not true. The “magic” is in relying on the Ruby hook provided by #inherited
. You won’t find #inherited
on ruby-doc.org because it is a private method (which ruby-doc.org does not list). Instead of calling this method you simply define a method and name it ‘inherited.’ You can see more detail at apidoc.com (this is also true of some of the other methods listed below). If you head over there, you’ll see this short description:
Callback invoked whenever a subclass of the current class is created.Warning: drastic oversimplification inbound! Rails does some pretty slick stuff with regards to building the
has_many
method. But in the end ActiveRecord::Base defines #inherited
, which includes has_many and other modules, like so[2]:
[gist id="2470568"]
When you inherit from ActiveRecord::Base the above method is fired. #initialize_generated_modules
sets up the methods/modules and throws them into your Model (allowing you to build your association). That is potentially a little confusing. In the end this is what is happening:
[gist id=”2470572″]
Pretty dern cool, imho. It is important to note that #inherited
is called before subclass is fully defined, so any conditional logic that involves the subclass will not work. Also, calling super
after is reccomended after executing your custom code.
included
Callback invoked whenever the receiver is included in another module or class.Every Ruby developer is familiar with
#include
, which allows you to inject a module’s instance methods into a class. Similarly, to include a module’s class methods you’d use #extend
. So, if you had a module that has both instance and class methods, you’d have to do something like the following to get the entire module into your class:
[gist id="2470578"]
Which brings us to our next hook, which allows us to execute arbitrary code whenever a module is included into a class. It’s defined on Module, and there are some idioms associated with its use. To accomplish the preceding example, you could simply define #included
:
[gist id=”2470592″]
This is just one use of #included
. Any code could be executed in the included hook, but this is a common idiom that you will see. When we include MyModule
into the Blah
class, we saw the hook fire, which calls extend on ‘base’. In this case, the argument ‘base’ represents the current self
which ends up being Blah
at the time included is called (yikes, we’ll talk more about self
in another post). Pretty Batman, right?.
extended
I won’t go into this at length due to its similarity to#included
. It will fire arbitrary code when the module is extended. It is my opinion that doing the inverse of the above (included) idiom is less acceptable. However, I think this is still an amazing tool. A completely contrived example could look like this:
[gist id=”2470596″]
If you execute the snippet above, you’ll see that #extended
gets called when Blah extends it. Pretty neat. Movin’ on.
extend_object
Extends the specified object by adding this module’s constants and methods (which are added as singleton methods).When you call Object#extend the extend_object, callback is fired. This is especially useful when you would like to extend an object, but only if it has certain characteristics. [gist id=”2470605″] This will actually extend the variable
my_extended_string
, which happens to be the string “blah”, with MyModule.
const_missing
This is very similar to method_missing except it is called when a constant is missing or undefined. The example the docs give is pretty hairy, so I’ll make my own very contrived example of what you might use this for. [gist id=”2470610″]Conclusions
These hooks are often used in some of the more clever pieces of Ruby code floating around. Utilizing them can help you write better code. Not only that, but using these methods will also help you solidify your understanding of how Ruby executes/loads/manages each program you write. In a talk given back in 2007, @pragdave explained some of the reasons why extending Ruby through metaprogramming is one of the mainstays of the language. He spoke of how metaprogramming in Ruby raises the level of abstraction. Essentially, this means increasing the distance between machine/language and your code. This is good for a few reasons. For one thing, the code ends up looking much more readable to both coders and clients. Also, once the underlying code is laid, a new developer has to understand less to be able to contribute. This raised level of abstraction is an amazing part of Ruby, and is prominently featured in some of its largest projects (especially Rails). I hope you find yourself exploring the hooks mentioned above in your own code. I’m sure that if you do, you’ll begin to think of Ruby a little more fondly. Thanks for reading. This is first edition of Hitchhiker’s Guide to Metaprogramming. The next edition will cover more of the fundamentals of metaprogramming in Ruby. Learn more about metaprogramming at my blog where I’ve posted several links I’ve found helpful while writing this post. ^_^Frequently Asked Questions about Metaprogramming Class/Module Hooks in Ruby
What is the main difference between a class and a module in Ruby?
In Ruby, both classes and modules are used to group methods, constants, and other class modules. However, the main difference lies in their usage. A class can be instantiated, meaning you can create new objects from the class, and it supports inheritance. On the other hand, a module cannot be instantiated and doesn’t support inheritance. Instead, modules provide a way to group code that can be mixed into classes and other modules, a feature known as mixins in Ruby.
Why would you wrap a class in a module in Ruby?
Wrapping a class inside a module is a common practice in Ruby, often used for namespacing. This helps prevent conflicts with classes of the same name, as the module creates a separate namespace for the class. It’s also useful for organizing related classes and modules, making your code easier to understand and maintain.
How does metaprogramming work in Ruby?
Metaprogramming in Ruby is a technique where programs have the ability to write, manipulate, or modify other programs (or themselves) during runtime. This is possible because everything in Ruby, including classes and instances of classes, are objects. This means that they can be manipulated just like any other object. Metaprogramming techniques include defining methods dynamically, using method_missing, and creating hooks.
What are class/module hooks in Ruby?
Class/module hooks are special methods in Ruby that get triggered when certain events occur, such as when a class inherits from another class or when a module is included in a class. These hooks include methods like included
, extended
, inherited
, and prepended
. They are used to add behavior or modify the state of the class or module when these events occur.
How do you use the included
hook in Ruby?
The included
hook in Ruby is a special method that gets triggered when a module is included in a class or another module. It’s often used to add instance methods to the including class or module. Here’s an example:module MyModule
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def my_method
'Hello, world!'
end
end
end
class MyClass
include MyModule
end
puts MyClass.my_method # => "Hello, world!"
In this example, when MyModule
is included in MyClass
, the included
hook is triggered, which extends MyClass
with the ClassMethods
module from MyModule
.
What is the extended
hook used for in Ruby?
The extended
hook in Ruby is a special method that gets triggered when a module is extended by an object. It’s often used to add methods to the object that extended the module. This is similar to the included
hook, but it works with extend
instead of include
.
How does the inherited
hook work in Ruby?
The inherited
hook in Ruby is a special method that gets triggered when a class is inherited by another class. It’s often used to add behavior or modify the state of the subclass. The inherited
method takes one argument, which is the subclass.
What is the prepended
hook in Ruby?
The prepended
hook in Ruby is a special method that gets triggered when a module is prepended to a class or another module. It’s often used to insert the module’s methods at the beginning of the ancestor chain, allowing them to override methods of the class or module they’re prepended to.
Can you use multiple hooks in the same class or module?
Yes, you can use multiple hooks in the same class or module in Ruby. Each hook will get triggered when its corresponding event occurs. For example, you can use both the included
and extended
hooks in the same module, and they will get triggered when the module is included and extended, respectively.
What are some use cases for metaprogramming and hooks in Ruby?
Metaprogramming and hooks in Ruby are powerful tools that can be used for a variety of purposes. They can be used to create DSLs (Domain Specific Languages), to add behavior to classes or modules dynamically, to track inheritance or module inclusion, to create plugins or extensions, and much more. However, they should be used with care, as they can make code more complex and harder to debug if used improperly.
Jonathan Jackson is a Ruby/Rails developer, working in Jacksonville Beach at Hashrocket. He often writes about Ruby development at his blog jonathan-jackson.net, which features articles designed to enlighten newbies and veterens alike. When he's not crunching code, he enjoys gaming, reading, and ultimate frisbee.