Nathan Long
Nathan Long

Reputation: 125902

Why doesn't Ruby module inheritance work like class inheritance?

Suppose I have a module called Flight with both class and instance methods. I can get its methods into a class using include, extend, or both:

class Bat < Mammal
  # Add Flight's class methods to Bat.
  extend Flight

  # Add Flight's instance methods to Bat.
  include Flight
  ...
end

include will add Flight to Bat.ancestors, but extend will not.

My question is, why is this different for modules than for classes? When I subclass Mammal, I always get both class and instance methods at once. However, when I mix in a module, I cannot get both class and instance methods at once (unless I use the self.included hook or something like ActiveSupport::Concern).

Is there a language-design issue behind this difference?

Upvotes: 5

Views: 8108

Answers (3)

megas
megas

Reputation: 21791

"When I subclass Mammal, I always get both class and instance methods at once"

That's because Bat class as an object also inherited the instance methods from Mammal singleton class.

Inheritance Graph

Including a module into a class changes the method look up chain. So actually the class doesn't inherit any instance methods.

Extending a class with a module is same as extending any object. The class simply acquires the module's instance methods as class instance methods (ie. methods on the class object itself).

Upvotes: 3

Alberto Moriconi
Alberto Moriconi

Reputation: 1655

Both Module#include and Object#extend are used to add the instance methods of a Module to an Object. Given the module:

module Flight
    def can_fly?
        true
    end
end

Module#include is used to add (or mix in) the instance methods of a module to the instance methods of a class or a module:

class Bat < Mammal
    include Flight
end

a = Bat.new()
a.can_fly?        # true

It actually affects the Object#is_a? method, so:

a.is_a? Flight     # true

Module#include is a private method, so it can only be called with function notation when defining a class or another module:

class Bat < Mammal
    self.include Flight     # NoMethodError: private method called
end

Object#extend adds the instance methods of a module as singleton methods to the object on which it's called, so you can do this:

b = Mammal.new()
b.extend Flight
b.can_fly?           # true
b.is_a? Flight       # true

c = Mammal.new()
c.can_fly?           # NoMethodError: undefined method

And only b will have the instance methods from Flight; other Mammal objects won't.

When calling Object#extend inside a class definition, the methods are added to the eigenclass of the class you're defining. This is the important difference between the two methods when using them inside a class definition, because the methods are added as class methods:

class Bat < Mammal
    extend Flight
end

Bat.can_fly?     # true

d = Bat.new
d.can_fly?       # NoMethodError: undefined method

Upvotes: 10

severin
severin

Reputation: 10258

I would like to address one part of your question:

include will add Flight to Bat.ancestors, but extend will not.

extend is not the same as include so it does something different obviously... You can think of extend being equal to an include on the class' metaclass.

Have a look at the following example:

module M
end

class A
  include M
end

# then you will see M within A's ancestors as you know
A.ancestors # => [A, M, Object...]


class B
  # the following is roughly the same as extend M:
  class <<self
    include M
  end
end

# then you will see M within B's metaclass' ancestors
MetaclassOfB = class <<B; self; end
MetaclassOfB.ancestors # => [M, Class, Module...]

So, since extend is like an include on the metaclass, you see the extended modules showing up in the metaclass' ancestor chain...

Upvotes: 3

Related Questions