Skip to content

Mixins: include, extend, prepend

Level: 201 · a reader who knows Python's multiple inheritance and wants to see what Ruby does instead

One line: include inserts a module into the class's ancestor chain behind the class, prepend inserts it in front so the module's method runs first and super reaches the class, and extend does an include into one object's singleton class.

Ruby has single inheritance and modules. A module holds methods but cannot be instantiated or subclassed; a class mixes it in, and the effect is nothing more than a new entry in ancestors, the list a method call walks from left to right. include Greeting puts Greeting just after the class, so the class's own methods win and super inside them reaches the module. prepend Loud puts Loud before the class, so Loud#greet runs first and its super reaches the class's greet — the way to wrap a method without touching it. extend Shouting on one object mixes the module into that object's singleton class, so only that object gains the methods; extend inside a class body does the same to the class object, which is how a module supplies class methods. Comparable and Enumerable are the standard library's two famous mixins: each asks for one method (<=> or each) and gives back a family. The program builds the chain step by step and records the order in which the methods actually run.

Verified output of mixins_include_extend_prepend_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

 1. include Greeting -- Person.ancestors.first(3)              [Person, Greeting, Object]; ann.greet: "hello from ann"
 2. Person.include?(Greeting) / included_modules - Object's    true / [Greeting]
 3. bob.extend(Shouting) -- bob.shout / ann.shout              "BOB!" / NoMethodError: undefined method 'shout' for an instance of Person
 4. extend in a class body -- Registry.count                   "Registry counts"; singleton_class.include?: true
 5. prepend Loud -- Person.ancestors.first(4)                  [Loud, Person, Greeting, Object]
 6. ann.greet: the result / the order the methods ran          "PERSON SAYS HELLO FROM ANN" / [:Loud, :Person, :Greeting]
 7. include Greeting a second time -- ancestors grew?          false
 8. include Comparable + <=> -- v1 < v2 / v5.clamp(v1, v3)     true / v3
 9. include Enumerable + each -- sort / include?("a")          ["a", "b", "c"] / true
10. a module is not a class: Class.new(Greeting)               TypeError: superclass must be an instance of Class (given an instance of Module)

Reading the output

Row 1 is include: Person.ancestors reads [Person, Greeting, Object], and ann.greet finds greet in the second entry. Row 2 shows the two questions you can ask a class about its mixins. Row 3 is extend on one object: bob.shout works and ann.shout, on another instance of the same class, does not — the module went into bob's singleton class alone. Row 4 is extend in a class body: Registry.count is now a class method, and Registry.singleton_class.include?(Counting) shows where it went.

Rows 5 and 6 are prepend. The chain now begins [Loud, Person, Greeting, Object], and when ann.greet is called the trace records Loud first, then Person, then Greeting: each level called super, each super went one step to the right, and the final string was built from the inside out and upcased on the way back. Row 7 shows that a module already in the chain is not added twice — include is idempotent.

Rows 8 and 9 are the standard mixins. Version defines only <=>, and Comparable supplies <, clamp and the rest; Deck defines only each, and Enumerable supplies sort, include? and the rest of its toolbox (Enumerable is a mixin shows what each buys). Row 10 is the boundary: a module is not a class, so Class.new(Greeting) — trying to use it as a superclass — is a TypeError.

Compared with Python

A Python mixin is an ordinary class listed among the bases, and the ancestor chain is __mro__, computed by the C3 algorithm from the order of those bases. Rows 1 and 2 are therefore the same picture with different names: ['Person', 'Greeting', 'object'], issubclass. Row 3 has no direct counterpart — Python cannot mix a class into one instance — and the closest thing is binding a single function onto the object with types.MethodType, which the twin does. Row 4 gets class methods from a base class that defines @classmethods, where Ruby uses extend. The real difference is rows 5 and 6: there is no prepend. With class Person(Loud, Greeting) the MRO reads ['Person', 'Loud', 'Greeting', 'object'] — the class itself always comes first — so Person.greet runs before Loud.greet, and the trace and the result string come out in the opposite order to Ruby's. To wrap a method from outside in Python you subclass, or you decorate it. Row 7 is a contrast too: listing a base twice is a TypeError, where Ruby's second include is a no-op. Rows 8 and 9 show Python's counterparts to the standard mixins — functools.total_ordering (two methods in, six out; no clamp, so the twin nests min and max) and collections.abc.Sequence (__getitem__ and __len__ in; __contains__, __iter__, index and count out). Row 10 closes the loop: a Python mixin is a class, so Greeting() is a perfectly good instance and Greeting a perfectly good base.

Verified output of mixins_include_extend_prepend_py.py — regenerated by tools/run_examples.py, never hand-typed.

 1. class Person(Greeting) -- Person.__mro__[:3]               ['Person', 'Greeting', 'object']; ann.greet(): 'hello from ann'
 2. issubclass(Person, Greeting) / bases beyond object         True / ['Greeting']
 3. bob.shout = MethodType(...) -- bob.shout() / ann.shout()   'BOB!' / AttributeError
 4. a base with @classmethod -- Registry.count()               'Registry counts'; kind: classmethod
 5. class Person(Loud, Greeting) -- __mro__[:4]                ['Person', 'Loud', 'Greeting', 'object']
 6. Person("ann").greet(): the result / the order              'person says HELLO FROM ANN' / ['Person', 'Loud', 'Greeting']
 7. class Twice(Greeting, Greeting) -- a base listed twice     TypeError
 8. @total_ordering + __lt__/__eq__ -- v1 < v2 / min(max())    True / v3
 9. Sequence + __getitem__/__len__ -- sorted / "a" in          ['a', 'b', 'c'] / True
10. a mixin is a class: Greeting() / a subclass of it          Greeting instance / Sub

The MRO is explained in The Python 2.3 Method Resolution Order ↗ and cooperative super() in super ↗.

Try it

  1. Write a Timestamped module whose save wraps super's result. prepend it to one class and include it in another, each with its own save; print ancestors.first(2) and the result of save for both, and explain why the included version never runs.
  2. Give Greeting a self.included(base) hook that calls base.extend(ClassMethods), so that one include supplies both instance methods and class methods — then check Person.singleton_class.include?(Greeting::ClassMethods). (Hooks measures the hooks.)
Solution to 1

mixins_include_extend_prepend_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# Exercise 1: a Timestamped module whose `save` runs first when prepended and
# is shadowed by the class's own `save` when merely included.

module Timestamped
  def save
    "timestamped(" + super + ")"
  end
end

class PrependedRecord
  prepend Timestamped

  def save = "PrependedRecord#save"
end

class IncludedRecord
  include Timestamped

  def save = "IncludedRecord#save"
end

puts "PrependedRecord.ancestors.first(2): #{PrependedRecord.ancestors.first(2).inspect}"
puts "PrependedRecord.new.save:           #{PrependedRecord.new.save}"
puts "IncludedRecord.ancestors.first(2):  #{IncludedRecord.ancestors.first(2).inspect}"
puts "IncludedRecord.new.save:            #{IncludedRecord.new.save}"
puts "included: Timestamped#save is reached only through super, which IncludedRecord#save never calls"

Verified output of mixins_include_extend_prepend_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

PrependedRecord.ancestors.first(2): [Timestamped, PrependedRecord]
PrependedRecord.new.save:           timestamped(PrependedRecord#save)
IncludedRecord.ancestors.first(2):  [IncludedRecord, Timestamped]
IncludedRecord.new.save:            IncludedRecord#save
included: Timestamped#save is reached only through super, which IncludedRecord#save never calls

See also