Explaining Include and Extend
All Rubyists should be familiar with the common definitions for include
and extend
.
You include
a module to add instance methods to a class and extend
to add class methods.
Unfortunately, this common definition isn’t entirely accurate.
It fails to explain why we use instance.extend(Module)
to add methods to an instance.
Shouldn’t it be instance.include(Module)
?
To figure this out we’re going to start by discussing where methods are stored.
The methods I’m storing for you and the guy holding mine.
Objects in Ruby do not store their own methods. Instead, they create a singleton class to hold onto their methods.
class A
def self.who_am_i
puts self
end
def speak_up(input)
puts input.upcase
end
end
The interpreter will create an A
class and a singleton class to attach to it (we’ll refer to the singleton class of an object by prepending '
to the object name).
Any instance methods (i.e. speak_up
) are added to the methods stored on A
.
The class methods (i.e. who_am_i
) are stored on 'A
.
> A.singleton_methods # methods on 'A
# => [:who_am_i]
This same process happens with instance objects.
If we have an instance of A
and we add a method to it we can’t store it on the instance.
Remember, objects in Ruby do not store their own methods.
a = A.new
def a.not_so_loud(input)
puts input.downcase
end
Once again we create a singleton class for a
to store the not_so_loud
method.
Now we have a method that only belongs to a
and does not affect other instances of A
.
I’m my own father?
The A
class holds the methods and knows the inheritance chain needed for a
and all other instances of A
.
Similarly the singleton class, 'A
, holds the methods and knows the inheritance chain for A
.
You can think of A
as an instance of 'A
.
The catch is that we don’t talk directly to 'A
.
This means we need some way to distinguish between adding methods to A
and 'A
.
That’s where include
and extend
come in.
include
When you include
a module in an object, you’re adding the methods into the inheritance chain that the object is tracking.
class A
include M
end
We can see that this is the case by checking A
’s ancestors.
> A.ancestors
# => [A, M, Object, Kernel, BasicObject]
extend
Using extend
is the same as doing an include
on the object’s singleton class.
class A
extend M
end
Once again we can confirm this by checking 'A
’s ancestors.
> A.singleton_class.ancestors
# => [M, Class, Module, Object, Kernel, BasicObject]
We can also call extend
on an instance.
a = A.new
a.extend(M)
> a.singleton_class.ancestors
# => [M, A, Object, Kernel, BasicObject]
If you think of extend
as a way to add class methods then what we just did doesn’t make much sense.
Once you reframe it as adding methods to the singleton of an object the picture above becomes more clear.
Included Hook
Any time you call include
it will check the module to see if there is a method named included
.
This method is executed when the module is included.
It’s like an initialize
for includes.
As you might suspect, extend
has its own version of this called extended
.
When you want to add both instance and class methods you can do so using the included hook.
module M
def self.included(base)
base.extend(ClassMethods)
end
def speak_up(input)
puts input.upcase
end
module ClassMethods
def who_am_i
puts self
end
end
end
class C
include M
end
c = C.new
This works by first including M
into C
’s inheritance chain.
Then we extend
from C
which adds the methods to 'C
’s inheritance chain.
Conclusion
When you start to dig past the common uses include
and extend
can seem odd and intimidating.
Once you understand the underlying implementation it all starts to make better sense.
Let’s define include
and extend
once more.
include
: adds methods from the provided Module to the object
extend
: calls include
on the singleton class of the object
If you want a more details about what the interpreter is doing I recommend watching Patrick Farley’s talk on Ruby Internals.