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¶
- Write a
Timestampedmodule whosesavewrapssuper's result.prependit to one class andincludeit in another, each with its ownsave; printancestors.first(2)and the result ofsavefor both, and explain why the included version never runs. - Give
Greetingaself.included(base)hook that callsbase.extend(ClassMethods), so that oneincludesupplies both instance methods and class methods — then checkPerson.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¶
- Method lookup and
super— the full chain,superwith and without arguments, and where lookup ends - Classes are open — the other way to add methods to an existing class
- Enumerable is a mixin — row 9 as a whole lesson, with the list of what
eachbuys - Singleton classes — where
extendputs a module, and whatclass << selfhas to do with it - Comparable and the spaceship — row 8 as a whole lesson
- What a trait is ↗ — Rust's traits are the third design: a mixin that is also a type check
- A trait must be in scope ↗ — a Rust trait's methods appear only where the trait is imported, closer to a refinement than to
include - A module is a namespace, and cannot be instantiated —
module Shopis a named box for classes, constants and methods - There is more than one way, and one obvious way — Ruby's idioms