Ruby mixins: extend and include

Daniel Brady picture Daniel Brady · Jul 9, 2013 · Viewed 7.4k times · Source

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

Patrick Oscity picture Patrick Oscity · Jul 9, 2013

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.