Ruby superpowers — with great power comes a great prepend
You were walking into a science experiments lab, when suddenly you’re bitten by a radioactive spider. The next day you wake up feeling weird until you realised you’re given superpowers by that bite. You could control the way you position yourself in your hierarchical family tree! So you could make yourself your father’s father (basically your grandfather…), isn’t that amazing?!?
Well that’s not a real superpower, but that’s what happens when you use the ruby prepend
method when handling a modules.
One of the first things that we learn in ruby is that everything inherits from Object
, and that’s totally true but the caveat here is what lies between our class and Object
. Lets see the following code:
class SomeClass
endSomeClass.ancestors
=> [SomeClass, Object, Kernel, BasicObject]
With this example we prove that our previous assumption was right. But what will happen when we add a module to that class?
module SomeModule
endclass SomeClass
include SomeModule
end
Will the hierarchy change? Where will the module be? So this is how the hierarchy looks like:
SomeClass.ancestors
=> [SomeClass, SomeModule, Object, Kernel, BasicObject]
As you can see SomeModule
will be placed between the class and the Object
. This is all good until you have some requirements for that module that include doing something before the class, or even surround a call to a function in the class with some behaviour from the module.
Wait, I’m here to read about the prepend
why are you still writing about the include
? Well, we’ll get to the prepend
in a moment, but for you to understand in what scenarios the prepend
comes in hard for the kill, I’ll show you how things were done before it appeared.
Say we have the following class:
class George
def talk
"I'm George."
end
end
To make George
talk (without having to wire him to a car battery 😱) we just need to call George.new.talk
and it would talk back just I’m George.
. This is all good but it’s not very polite, is it? Lets create a module that will bring some politeness into this world.
module Politizer
def talk
"Hello. #{super}. How are you today?"
end
end
Now lets make George
more polite.
# Making George politeGeorge.include(Politizer)george = George.newgeorge.talk # => "I'm George."
But…but…I’ve made you more polite with my Politizer
, what happened in here?! Well, as you must’ve guessed, we are not reaching the talk
method that we redefine in the module due to it’s hierarchy:
=> [George, Politizer, Object, Kernel, BasicObject]
So what can we do if the include
will always add the module’s methods behind the class itself? To achieve that, we’ll need to use the alias_method
method with a little magic.
Modules have a callback that is called when they are included, so we will work with that to achieve great things! That said, lets change our Politizer
and make George
polite.
module Politizer
def self.included(base)
base.class_eval do
alias_method :talk_without_polite, :talk
alias_method :talk, :talk_with_polite
end
end
def talk_with_polite
"Hello. #{talk_without_polite}. How are you today?"
end
end# Making George politeGeorge.include(Politizer)george = George.newgeorge.talk # => "Hello. I'm George. How are you today?"
YEAH! George is now a polite fellow, but that code…damnnn it looks ugly as hell! That is why for these type of situations, in the Rails world, we have a alias_method_chain
method that makes things a liiiiiiiiitttle bit better:
class George
def talk
"I'm George"
end
endmodule Politizer
def self.included(base)
base.class_eval do
alias_method_chain :talk, :polite
end
end def talk_with_polite
"Hello. #{talk_without_polite}. How are you today?"
end
end# Making George politeGeorge.include(Politizer)george = George.newgeorge.talk # => "Hello. I'm George. How are you today?"
But now lets get down to business and talk about the thing you’re here for: the prepend
!! So, in ruby 2.0 (yes, I’m a little late to the hype train…) the prepend
method was introduced as the saviour for all of the trouble we had to make George
more polite.
With that in mind this is how we could add the Politizer
module to George
to achieve the behaviour that we want:
class George
def talk
"I'm George"
end
endmodule Politizer
def talk
"Hello. #{super}. How are you today?"
end
end# For ruby 2.0
George.send(:prepend, Politizer)# For ruby 2.1
George.prepend(Politizer)# Making George polite
george = George.new
george.talk
This code is cleaner and more understandable than the previous one, with the include
magic, right? So you must be wondering how this works, and what is the difference between include
and prepend
. The diference is…drum roll…object hierarchy! This is what object hierarchy with the prepend
method looks like:
George.ancestors
=> [Politizer, George, Object, Kernel, BasicObject]
# George with include
# => [George, Politizer, Object, Kernel, BasicObject]
You see the diferences, right? So now when we call george.talk
we are looking first in the module instead of the class itself, and with the wonders of hierarchies we can call super
to call the methods on the parent class itself.
As usual if you find this useful smash that 💚 button, and if you have anything to say just leave a comment or drop it like it’s hot @RicBrazao!