|
|||
Previous < |
Contents ^
|
Next >
|
sin
, cos
, and so on.
You stuff them all into a file, trig.rb
, for future generations
to enjoy. Meanwhile, Sally is working on a simulation of good and evil,
and codes up a set of her own useful routines, including beGood
and sin
, and sticks them into action.rb
. Joe, who
wants to write a program to find out how many angels can dance on the
head of a pin, needs to load both trig.rb
and action.rb
into his program. But both define a method called sin
. Bad news.
The answer is the module mechanism. Modules define a namespace, a
sandbox in which your methods and constants can play without having to
worry about being stepped on by other methods and constants. The trig
functions can go into one module:
module Trig PI = 3.141592654 def Trig.sin(x) # .. end def Trig.cos(x) # .. end end |
module Action VERY_BAD = 0 BAD = 1 def Action.sin(badness) # ... end end |
require
statement, which we discuss
on page 103) and reference the qualified names.
require "trig" require "action" y = Trig.sin(Trig::PI/4) wrongdoing = Action.sin(Action::VERY_BAD) |
module Debug
|
||
def whoAmI?
|
||
"#{self.type.name} (\##{self.id}): #{self.to_s}"
|
||
end
|
||
end
|
||
class Phonograph
|
||
include Debug
|
||
# ...
|
||
end
|
||
class EightTrack
|
||
include Debug
|
||
# ...
|
||
end
|
||
ph = Phonograph.new("West End Blues")
|
||
et = EightTrack.new("Surrealistic Pillow")
|
||
|
||
ph.whoAmI?
|
» |
"Phonograph (#537766170): West End Blues"
|
et.whoAmI?
|
» |
"EightTrack (#537765860): Surrealistic Pillow"
|
Debug
module, both Phonograph
and
EightTrack
gain access to the whoAmI?
instance method.
A couple of points about the include
statement before we go on.
First, it has nothing to do with files. C programmers use a
preprocessor directive called #include
to insert the contents of
one file into another during compilation. The Ruby include
statement simply makes a reference to a named module. If that module
is in a separate file, you must use require
to drag that
file in before using include
. Second, a Ruby include
does
not simply copy the module's instance methods into the class. Instead,
it makes a reference from the class to the included module. If
multiple classes include that module, they'll all point to the same
thing. If you change the definition of a method within a module, even
while your program is running, all classes that include that module
will exhibit the new behavior.[Of course, we're speaking only
of methods here. Instance variables are always per-object, for
example.]
Mixins give you a wonderfully controlled way of adding functionality
to classes. However, their true power comes out when the code in the
mixin starts to interact with code in the class that uses it. Let's
take the standard Ruby mixin Comparable
as an
example. The Comparable
mixin can be used to add the comparison
operators (<
, <=
, ==
, >=
, and >
), as well as
the method between?
, to a class. For this to work,
Comparable
assumes that any class that uses it defines the
operator <=>
. So, as a class writer, you define the one method,
<=>
, include Comparable
, and get six comparison functions for
free. Let's try this with our Song
class, by making the songs comparable
based on their duration.
All we have to do is include the Comparable
module and implement
the comparison operator <=>
.
class Song include Comparable def <=>(other) self.duration <=> other.duration end end |
song1 = Song.new("My Way", "Sinatra", 225)
|
||
song2 = Song.new("Bicylops", "Fleck", 260)
|
||
|
||
song1 <=> song2
|
» |
-1
|
song1 < song2
|
» |
true
|
song1 == song1
|
» |
true
|
song1 > song2
|
» |
false
|
inject
function, implementing it
within class Array
. We promised then that we'd make it more generally
applicable. What better way than making it a mixin module?
module Inject def inject(n) each do |value| n = yield(n, value) end n end def sum(initial = 0) inject(initial) { |n, value| n + value } end def product(initial = 1) inject(initial) { |n, value| n * value } end end |
class Array
|
||
include Inject
|
||
end
|
||
[ 1, 2, 3, 4, 5 ].sum
|
» |
15
|
[ 1, 2, 3, 4, 5 ].product
|
» |
120
|
class Range
|
||
include Inject
|
||
end
|
||
(1..5).sum
|
» |
15
|
(1..5).product
|
» |
120
|
('a'..'m').sum("Letters: ")
|
» |
"Letters: abcdefghijklm"
|
Enumerable
module, which starts
on page 403.
self
.
For a mixin, this means that the module that you mix into your
client class (the mixee?) may create instance variables in the client
object and may use attr
and friends to define accessors for
these instance variables. For instance:
module Notes attr :concertA def tuning(amt) @concertA = 440.0 + amt end end class Trumpet include Notes def initialize(tune) tuning(tune) puts "Instance method returns #{concertA}" puts "Instance variable is #{@concertA}" end end # The piano is a little flat, so we'll match it Trumpet.new(-5.3) |
Instance method returns 434.7 Instance variable is 434.7 |
module MajorScales def majorNum @numNotes = 7 if @numNotes.nil? @numNotes # Return 7 end end module PentatonicScales def pentaNum @numNotes = 5 if @numNotes.nil? @numNotes # Return 5? end end class ScaleDemo include MajorScales include PentatonicScales def initialize puts majorNum # Should be 7 puts pentaNum # Should be 5 end end ScaleDemo.new |
7 7 |
@numNotes
. Unfortunately, the result is probably
not what the author intended.
For the most part, mixin modules don't try to carry their own instance
data around---they use accessors to retrieve data from the client
object. But if you need to create a mixin that has to have its own
state, ensure that the instance variables have unique names to
distinguish them from any other mixins in the system (perhaps by using
the module's name as part of the variable name).
Enumerable
. All you have
to do is write an iterator called each
, which returns the
elements of your collection in turn. Mix in Enumerable
, and
suddenly your class supports things such as map
,
include?
, and find_all?
. If the objects in your collection
implement meaningful ordering semantics using the <=>
method, you'll also get min
, max
, and
sort
.
load "filename.rb" require "filename" |
load
method includes the named Ruby source file every
time the method is executed, whereas require
loads any given
file only once.
require
has additional functionality: it can load
shared binary libraries. Both routines accept relative and absolute
paths. If given a relative path (or just a plain name), they'll search
every directory in the current load path ($:
, discussed
on page 140) for the file.
Files loaded using load
and require
can, of course, include
other files, which include other files, and so on. What might
not be obvious is that require
is an executable
statement---it may be inside an if
statement, or it may include a
string that was just built. The search path can be altered at runtime
as well. Just add the directory you want to the string $:
.
Since load
will include the source unconditionally, you can
use it to reload a source file that may have changed since the
program began:
5.times do |i| File.open("temp.rb","w") { |f| f.puts "module Temp\ndef Temp.var() #{i}; end\nend" } load "temp.rb" puts Temp.var end |
0 1 2 3 4 |
Previous < |
Contents ^
|
Next >
|