DesAdams DesAdams - 3 months ago 18
Ruby Question

Ruby mixins: extend and include

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?

Answer

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.