Lesson 03 / 09
Modules and Mixins
How a module differs from a class; mixing a module into the ancestor chain with include; Comparable and <=>; order among two modules of the same name; extend on a single object.
Published Updated
In this lesson
- Show with ancestors and include? that a module enters the ancestor chain of a class through include
- Measure that a module has no instances and that a call to new raises NoMethodError
- Show that when two modules carry the same name, the order in the chain decides the result
- Measure that mixing a module into a single object with extend does not affect other objects
Contents
The previous lesson left a fact behind: report was not written in the Account class, yet the call account.report gave report: amy 100. Reportable, which stands between Account and Object in the chain, carries the method, but what exactly does it mean for a module to give a method to a class? If a module is not a class, how does it enter the chain?
The author of this lesson is again the ancestor, but the type of the ancestor changes: the definition sits not in a superclass but in a mixed-in module. The lesson first separates a module from a class, then measures the place and the order of a mixin in the chain.
A Mixed-In Module Becomes the Author
The file below mixes the Reportable module into the Account class with include. author.rb is the rule from the earlier lessons and is loaded unchanged here as well.
# author.rb — a helper that classifies who wrote a method
# Classification asks in order: is there a definition in the receiver's chain at all,
# then does the receiver's own class carry it, then how was the definition produced.
def author(receiver, name)
chain = receiver.singleton_class
unless chain.method_defined?(name) || chain.private_method_defined?(name)
return ["nobody", "no definition"]
end
definition = chain.instance_method(name)
owner = definition.owner
place = owner == chain ? "singleton class" : owner.to_s
return ["ancestor", place] unless owner == receiver.class || owner == chain
iseq = RubyVM::InstructionSequence.of(definition)
return ["code", "#{place}, no ruby body"] if iseq.nil?
return ["code", "#{place}, block body"] if iseq.label.start_with?("block")
["programmer", "#{place}, def body"]
end
# mixin.rb — the author of a module mixed in with include
require_relative "author"
module Reportable
def report = "report: #{@owner} #{@balance}"
end
class Account
include Reportable
def initialize(owner, balance)
@owner = owner
@balance = balance
end
end
account = Account.new("amy", 100)
puts account.report
p author(account, :report)
p Account.include?(Reportable)
p Object.include?(Reportable)
p Account.ancestors
report: amy 100 ["ancestor", "Reportable"] true false [Account, Reportable, Object, Kernel, BasicObject]
author gave the ancestor author with Reportable as the evidence: the definition is written not in the body of Account but in the module. Account.include?(Reportable) came out true and Object.include?(Reportable) false; a mixin affects only the class that receives the include call. In the Account.ancestors list, Reportable stands between Account and Object. Mixing a module into a class means placing the module directly above that class.
CM7. In this lesson “to mix in” means placing a module, with an include call, in the ancestor chain of a class, directly above the class. The line of code that does it is include Reportable, and the result is the new place in the chain. In the language of the rule the author of a mixin comes out as an ancestor, because the definition is not in the receiver’s class but in the upper part of the chain.
That a mixin adds a link to the chain and does not copy the methods into the class is seen through two further questions. include is an expression and returns a value; instance_methods(false) asks for the body of the class itself. The file below measures both.
# no_copy.rb — include adds a link to the chain, it does not copy the method module Reportable def report = "report" end class Account p(include(Reportable)) end p Account.instance_methods(false) p Reportable.instance_methods p Account.instance_method(:report).owner p Account.included_modules.take(2)
Account [] [:report] Reportable [Reportable, Kernel]
The include call returned the receiver, that is, the Account class. Account.instance_methods(false) came out empty: report was not moved into the body of the class; it stayed in the module. Reportable.instance_methods showed the method in the module’s own list, and instance_method(:report).owner confirmed that the owner is Reportable. included_modules gave the modules in the chain in the order Reportable, Kernel. So the same situation as with the empty subclass of the previous lesson holds: being callable and being written in the class are separate things, and a module does not move what is written; it changes the length of the chain.
A Module Is Not a Class
A mixin makes the module look like a class, but the two are not the same thing. No object can be produced from a module; a module exists only to enter the chain of others. The file below measures this with two questions.
# module_not_class.rb — a module has no instances module Reportable def report = "report" end p Reportable.instance_of?(Class) p Reportable.instance_of?(Module) begin Reportable.new rescue NoMethodError => e puts e.class end
false true NoMethodError
Reportable is an instance of Module, not an instance of Class, and the new method is not defined on it. This difference also explains why a mixin is needed: inheritance accepts only one superclass, whereas any number of modules can be mixed into a class. Independent behaviors, for example reporting and comparing, are not squeezed into a single superclass chain; they are written in separate modules and mixed into the class that needs them.
A Mixin from the Core: Comparable
A mixin is not limited to modules we write ourselves. The Comparable module of the core gives a class the rest of the comparison methods when only <=> is written. The file below measures this on a small Score class.
# compare.rb — Comparable and <=>
require_relative "author"
class Score
include Comparable
attr_reader :value
def initialize(value)
@value = value
end
def <=>(other) = value <=> other.value
end
a = Score.new(3)
b = Score.new(5)
c = Score.new(9)
p c.between?(a, c)
p b > a
p [c, a, b].min.value
p author(a, :between?)
p author(a, :<=>)
true true 3 ["ancestor", "Comparable"] ["programmer", "Score, def body"]
Only <=> is written inside Score, yet the calls between? and >, and the min call that works over <=>, gave results. For between? the author is an ancestor with Comparable as the evidence; for <=> it is the programmer, because Score wrote it in its own body. Two methods in the same class, two separate authors: a mixin does not delete the class’s own definition; it adds next to it.
Comparable gives not only between? but all the comparisons built over <=>. The file below measures two more behaviors that are never written in Score: equality and clamping into a range.
# compare_more.rb — Comparable provides three more methods
require_relative "author"
class Score
include Comparable
attr_reader :value
def initialize(value)
@value = value
end
def <=>(other) = value <=> other.value
end
a = Score.new(3)
b = Score.new(5)
p a == Score.new(3)
p a.equal?(Score.new(3))
p Score.new(9).clamp(a, b).value
p author(a, :==)
p Score.ancestors.take(3)
true false 5 ["ancestor", "Comparable"] [Score, Comparable, Object]
a == Score.new(3) gave true, because Comparable#== tests whether the result of <=> on the two objects is zero. equal?, however, showed that they are two separate objects and gave false: equality is asked by value, identity by object. clamp brought the value nine down to five. For ==, author pointed at Comparable; BasicObject also defines an ==, but because Comparable comes first in the chain, that one is shadowed. This is the core example of the order rule measured below.
The Same Name in Two Modules
If two modules are mixed into a class and both define the same name, which one wins? The order in the chain decides, and the order of mixing decides the order of the chain: the one mixed in later goes in front of the earlier one.
# two_modules.rb — the same name, two modules module A1 def label = "A1" end module B1 def label = "B1" end class C1 include A1 include B1 end p C1.ancestors.take(3) puts C1.new.label p C1.ancestors.count(B1)
[C1, B1, A1] B1 1
include A1 was written first and include B1 second, and the chain came out as [C1, B1, A1]: the one mixed in last stands closest to the receiver. The label call therefore gave B1. ancestors.count(B1) gave one: a module stands in the chain as a single link. Whether mixing in the same module a second time changes this table is asked by the measurement below.
# repeated_include.rb — a second include does not change the chain module A1 def label = "A1" end module B1 def label = "B1" end class C1 include A1 include B1 include A1 end p C1.ancestors.take(3) puts C1.new.label p C1.ancestors.count(A1)
[C1, B1, A1] B1 1
The second include A1 at the end moved nothing in the chain. Because A1 was already in the chain, Ruby did not bring it to the front again, so B1 kept winning and ancestors.count(A1) gave one. A mixin holds a module in the chain at most once; the way to change the order is not to include again.
CM8. In this lesson the “order in the chain” is the reading of the ancestors array from left to right, and method lookup walks in that order. The first matching definition wins. Because a module included later takes its place directly above the class, it shadows the earlier one.
super in Modules
A method inside a module can also call super, and super descends to the next link in the current chain. This gives two different results depending on whether the module comes before or after the class. The file below combines two modules with a superclass and a subclass.
# module_super.rb — super passes through the modules in the chain module A2 def label = "A2 " + super end module B2 def label = "B2 " + super end class Base def label = "base" end class C2 < Base include A2 include B2 end class D2 < Base include A2 def label = "D2 " + super end puts C2.new.label p C2.ancestors.take(4) puts D2.new.label
B2 A2 base [C2, B2, A2, Base] D2 A2 base
For C2 the call entered B2, descended with super to A2, and from there to Base, and the text was assembled in the order of the chain. ancestors showed the same path as [C2, B2, A2, Base]: super is walking along the chain. In D2 the class wrote its own label; because the module stays below the class, D2 ran first, and with super the call went down to A2 and from there to Base. Combined with the super_method measurement of the previous lesson, the result is this: both a subclass that overrides and a module that is mixed in are links of the same chain, and super does not tell the two apart.
CM14. In this lesson “super in the chain” means a super call descending to the next link in the ancestors order. Whether the link is a class or a module does not change the call; only the order decides.
Modules can also be mixed into one another. The chain of a module carries the modules mixed into it, and the class that includes that module inherits all of them at once. The file below measures a two-layer mixin.
# nested.rb — a module mixed into a module comes along with it to the class module Upper def upper = "upper" end module Lower include Upper def lower = "lower" end class Box include Lower end p Lower.ancestors p Box.ancestors.take(3) p Box.new.upper p Box.include?(Upper)
[Lower, Upper] [Box, Lower, Upper] "upper" true
Lower.ancestors showed the module’s own chain, and Upper stood in it directly behind Lower. Box mixed in only Lower, yet Upper entered its chain too; that is why Box.new.upper worked and Box.include?(Upper) gave true. It can be seen here that a mixin brings not a single link but the whole of the module’s current chain. When reading the chain of a class, the mixins of the modules have to be counted as well.
The Bounding Measurement
There are two limits. The first: if the class writes its own report, what happens to the definition of the mixin? The second: if a module is mixed not into the whole class but into a single object only, how do the author and the chain look?
# bounding.rb — the class shadows, extend mixes into a single object
require_relative "author"
module Reportable
def report = "report: #{@owner} #{@balance}"
end
class Account2
include Reportable
def initialize(owner, balance)
@owner = owner
@balance = balance
end
def report = "account2 report"
end
class Account3
def initialize(owner, balance)
@owner = owner
@balance = balance
end
end
p author(Account2.new("amy", 100), :report)
single = Account3.new("tom", 50)
single.extend(Reportable)
puts single.report
p author(single, :report)
p Account3.new("a", 1).respond_to?(:report)
["programmer", "Account2, def body"] report: tom 50 ["ancestor", "Reportable"] false
When Account2 wrote its own report, the rule gave the programmer: the class’s own definition comes before the module it mixed in and shadows it; Reportable#report keeps standing in the chain and can be reached only with a super call. single.extend(Reportable), in turn, mixed the module into the single object only; that object gave the answer report: tom 50, the ancestor author came out as Reportable, but a new object produced from the same class answered false to the question respond_to?(:report). The chain of this object has become different from that of its class; where that chain is placed will be covered in the coming lessons under the name of the singleton class.
What extend does to an object can be measured with the same questions as include: is the object counted as an instance of the module, are the other objects of the class counted so, and where does the module stand in the object’s own chain?
# extend_measure.rb — extend adds a link only to the chain of that object module Reportable def report = "report" end class Account3 end single = Account3.new other = Account3.new single.extend(Reportable) p single.is_a?(Reportable) p other.is_a?(Reportable) p single.singleton_class.include?(Reportable) p single.singleton_class.ancestors.drop(1).take(2) p Account3.include?(Reportable) p other.singleton_class.ancestors.drop(1).take(2)
true false true [Reportable, Account3] false [Account3, Object]
single behaved like an instance of the module, other did not, and the Account3 class never saw the module. After the first link of the object’s own chain came Reportable and then Account3; for other, in the same place there are directly Account3 and Object. So extend adds the module not to the chain of the class but to the chain particular to that single object. The structure that holds this chain is called the singleton class, and it will be examined separately in later lessons.
Choosing Between a Mixin and Inheritance
The choice between a module and a superclass depends on the kind of relation. TermDeposit is an Account: a specialized form of the same kind. Account, on the other hand, is something reportable: a capability of the kind. Capabilities recur across different kinds, for example an account and an invoice can both be reportable, yet they need not share the same superclass. Writing the capability in a module is the way to give it to two separate inheritance trees as something written in one place.
There is also a constraint: in Ruby a class has one superclass but can have as many mixins as wanted. The chain therefore always stays linear. Modules enter the chain as ordered links, and as seen in the measurement, this order is predictable: the one mixed in last comes first. If there were several superclasses, a separate rule would be needed to say along which path the chain walks; a single chain makes that unnecessary.
A mixin also has a cost: it scatters the author. To see which module a method of a class comes from, the body of the class is not enough; measurements such as ancestors and author are needed. The job of the author.rb used throughout this lesson is exactly this: to find the place of a definition by asking the running object, without looking at the body of the class.
Summary
- A module is an instance of
Module, not an instance ofClass;Reportable.newraisesNoMethodError.includemixes the module directly above the class, betweenAccountandObject;Account.include?(Reportable)is true andObject.include?(Reportable)is false. - The author of a mixin is an ancestor: for
account.reportthe evidence came out asReportable;Account.instance_methods(false)is empty, because the method is not copied into the class. - In the
Comparablemixin only<=>is written;between?and==have an ancestor author, and<=>has a programmer author. - If the same name exists in two modules, the one mixed in later wins (
[C1, B1, A1]); the same module stands in the chain once, and a secondincludedoes not change the order. - Inside a module,
superdescends to the next link in the chain; a module mixed into a module comes to the class together with it (Box.include?(Upper)). - If the class writes its own definition, it shadows the mixin and the author is the programmer;
extendmixes the module into a single object only.
Next Step
The Account class has a method named fee, and when account.fee is called from outside it raises NoMethodError; even so, the net method can call it from inside the same object. The author is still the programmer, but who can call it, and why are some calls refused? The next lesson, Visibility, measures this question.
To keep your progress and take notes, Log in
My notes
Log in to take notes.