I've been reading a few articles about Ruby's mixin methods, extend
and include
, and I am still not quite sure about the behavior. I understand that extend
will add the instance methods of the given module as singleton methods to the module doing the extending, and that include
will essentially append the contents of a module (methods, constants, variables) to the one doing the including, effectively defining them in the receiver.
However, after some tinkering, trying to get a feel for how the behavior will manifest, I've got a few questions. Here is my testing setup:
module Baz
def blorg
puts 'blorg'
end
end
module Bar
include Baz
def blah
puts 'blah'
end
end
module Foo
extend Bar
end
class Bacon
extend Bar
end
class Egg
include Bar
end
So as I would expect, module Bar
gains the instance methods defined in Baz
(#blorg
) as if they'd been defined in itself due to the inclusion method, and class Bacon
gains the singleton methods Bacon::blah
and Bacon::blorg
by extension.
Bacon.blah # => blah
Bacon.blorg # => blorg
And class Egg
gains the methods defined in Bar
(#blah
and now #blorg
) as instance methods.
Egg.new.blah # => blah
Egg.new.blorg # => blorg
I get all that, so that's good.
However, I don't understand the responses I get from using the #ancestors
and #is_a?
methods.
Bacon.ancestors # => [Bacon, Object, Kernel, BasicObject]
Bacon.is_a? Bar # => true
Egg.ancestors # => [Egg, Bar, Baz, Object, Kernel, BasicObject]
Egg.is_a? Bar # => false
It would seem that extending a module causes the #is_a?
method to return true
when queried about that module, but it is not added to the ancestors of the class, and vice versa with regards to inclusion: the ancestors of the class contains the modules being included, but the #is_a?
method returns false
when queried. Why does this happen?
The difference is that include
will add the included class to the ancestors of the including class, whereas extend
will add the extended class to the ancestors of the extending classes' singleton class. Phew. Let's first observe what happens:
Bacon.ancestors
#=> [Bacon, Object, Kernel, BasicObject]
Bacon.singleton_class.ancestors
#=> [Bar, Baz, Class, Module, Object, Kernel, BasicObject]
Bacon.new.singleton_class.ancestors
#=> [Bacon, Object, Kernel, BasicObject]
Bacon.is_a? Bar
#=> true
Bacon.new.is_a? Bar
#=> false
And for the Egg
class
Egg.ancestors
#=> [Egg, Bar, Baz, Object, Kernel, BasicObject]
Egg.singleton_class.ancestors
#=> [Class, Module, Object, Kernel, BasicObject]
Egg.new.singleton_class.ancestors
#=> [Egg, Bar, Baz, Object, Kernel, BasicObject]
Egg.is_a? Bar
#=> false
Egg.new.is_a? Bar
#=> true
So what foo.is_a? Klass
actually does is to check whether foo.singleton_class.ancestors
contains Klass
. The other thing happening is that all the ancestors of a class become ancestors of an instances' singleton class when the instance is created. So this will evaluate to true for all newly created instances of any class:
Egg.ancestors == Egg.new.singleton_class.ancestors
So what does all this mean? extend
and include
do the same thing on different levels, i hope the following example makes this clear as both ways to extend a class are essentially equivalent:
module A
def foobar
puts 'foobar'
end
end
class B
extend A
end
class C
class << self
include A
end
end
B.singleton_class.ancestors == C.singleton_class.ancestors
#=> true
where class << self
is just the odd syntax to get to the singleton class. So extend
really just is a shorthand for include
in the singleton class.