Modules and Mixins

Total Page:16

File Type:pdf, Size:1020Kb

Modules and Mixins

Modules and Mixins

Modules in Ruby are a way to group together methods, classes and constants.

They are similar to namespaces in languages such as C++.

Here’s an example from Programming Ruby (p. 117 ff.).

Say you write a set of the trigonometry functions 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 she codes routines including be_good and sin, and sticks them into moral.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 moral.rb into his program. What’s the problem here?

Let’s consider how we might “solve” this. Take a minute, and write down all the possibilities you can think of.

Ruby’s answer is the module mechanism.

Modules define a namespace. What’s a namespace?

The trig functions can go into one module module Trig PI = 3.141592654 def Trig.sin(x) # .. end def Trig.cos(x) # .. end

Lecture 7 Object-Oriented Languages and Systems 1 end and the good and bad moral methods can go into another. module Moral VERY_BAD = 0 BAD = 1 def Moral.sin(badness) # ... end end

Look at the names of module constants and methods. What do they remind you of? Class constants and methods! Same conventions.

When a program needs to use these modules, it can simply load the two files using the Ruby require statement, and reference the qualified names. require 'trig' require 'moral' y = Trig.sin(Trig::PI/4) wrongdoing = Moral.sin(Moral::VERY_BAD)

What new naming convention do you see in the code above?

Mixins The most interesting use of modules is to define mixins.

When you include a module within a class, all its functionality becomes available to the class.

Not only can modules contain class methods; they can also contain instance methods.

Let’s contrast this with #include in (what languages?) How are mixins different?

CSC/ECE 517 Lecture Notes © 2007 Edward F. Gehringer 2 Let’s contrast this with multiple inheritance in (what languages?). How are mixins different?

So, instead of implementing interfaces as in Java, one uses mixins in Ruby when one wants to

Consider the following code: module Introspect def kind puts "#{self.class.name}" end end class Animal include Introspect def initialize(name) @name = name end end class Car include Introspect def initialize(model) @model = model end end d = Animal.new("Cat") c = Car.new("Ferrari") d.kind # kind method is available through … c.kind # .. the mixin Introspect

Lecture 7 Object-Oriented Languages and Systems 3 Comparable A good example of the power of modules is Comparable, from the Ruby library.

To use comparable in a class, the class needs to define a method called <=> (sometimes called “rocket”).

Once we define this method, we get a lot of useful comparison functions, such as <, >, <=, >=, == and the method between? for free.

What is this like in Java?

Here is an example. Suppose that we have a Line class: class Line def initialize(x1, y1, x2, y2) @x1, @y1, @x2, @y2 = x1, y1, x2, y2 end end

We compare two lines on the basis of their lengths.

We add the Comparable mixin as follows: class Line include Comparable def length_squared (@x2 - @x1) * (@x2 - @x1) + (@y2 - @y1) * (@y2 - @y1) end def <=>(other) self.length_squared <=> other.length_squared end end

<=> returns 1,0, or –1, depending on whether the first argument is greater than, equal to, or less than the second argument.

CSC/ECE 517 Lecture Notes © 2007 Edward F. Gehringer 4 We delegate the call to <=> of the Fixnum class, which compares the squares of the lengths.

Now we can use the Comparable methods on Line objects: l1 = Line.new(1, 0, 4, 3) l2 = Line.new(0, 0, 10, 10) puts l1.length_squared if l1 < l2 puts "Line 1 is shorter than Line 2" else if l1 > l2 puts "Line 1 is longer than Line 2" else puts "Line 1 is just as long as Line 2" end end

>>Line 1 is shorter than Line 2 Simulating Multiple Inheritance Modules can be used to simulate multiple inheritance in Ruby. Suppose one wants to add tags to Strings. Then one can define a Taggable module and include it into the class. require 'set' # A collection of unordered values with no duplicates

# Include this module to make your class taggable. The names of the # instance variable and the setup method are prefixed with "taggable_" # to reduce the risk of namespace collision. You must call # taggable_setup before you can use any of this module's methods. module Taggable attr_accessor :tags

def taggable_setup @tags = Set.new

Lecture 7 Object-Oriented Languages and Systems 5 end

def add_tag(tag) @tags << tag end

def remove_tag(tag) @tags.delete(tag) end end class TaggableString < String include Taggable def initialize(*args) super taggable_setup end end s = TaggableString.new('It was the best of times, it was the worst of times.') s.add_tag 'dickens' s.add_tag 'quotation' s.tags # => #

What happens if we execute puts s ?

Extending Specific Objects There are two common methods for specifying objects: set based or prototype based.

Within a set-based language:

CSC/ECE 517 Lecture Notes © 2007 Edward F. Gehringer 6  We describe a class that abstracts what we know to be true of the object we are attempting to specify.  We create instances of the object from the class to perform the actual work.  Just as classes describe objects, classes are described by metaclasses.  Each object instance is unique and holds its own internal values.  We can create more specific subclasses of a given class that either modify the properties of the parent or add additional properties.  When an object receives a message it does not understand, it consults its ancestors, asking each of them to handle the message.

Within a prototype-based language:  We create an object that is a concrete representation of the object we are attempting to specify, called a prototype.  We copy, or clone, the prototype to obtain multiple instances of the object.  Each instance holds only what is unique to itself and a reference to its prototype, called an extension.  There is no true distinction between an instance and a prototype. Any instance can be cloned, becoming the prototypical object for its duplicates.  When an object receives a message it does not understand, it delegates the message to its prototypes, asking each of them to grant it the ability to handle the message.

We can quickly see the philosophical differences between sets and prototypes.

In a set-based object system, we define objects top down by specifying the abstract and using that specification to create the specific.

For example, to describe a Cat object we collect properties that we believe to be common to all Cats and create a Cat class. Then we use that class to create unique Cats.

Lecture 7 Object-Oriented Languages and Systems 7 In a prototype object system, we define objects bottom up by specifying a specific instance and modifying it as necessary to represent other instances.

For example, to describe a Cat Escher we create an object that contains her properties (female, black fur, annoying).

If we later discover a Cat Berlioz, we specify what we know about Berlioz (male, orange fur) and assume that the remainder is identical to the prototypical Cat Escher.

This system naturally evolves the assumption that all Cats are annoying.

Some researchers believe that prototype object systems make better usage of the human ability to generalize from their experience with concrete situations.

How does this apply to Ruby?

Ruby can act as a set-based language: Using include augments a class definition, and hence adds functionality to all objects of a class.

But it can also act as a prototype-based language: There is a way to add functionality to just specific instances of a class, by using the Object#extend method.

Let us suppose that there is a mild-mannered Person class. class Person attr_reader :name, :age, :occupation

def initialize(name, age, occupation) @name, @age, @occupation = name, age, occupation end

def mild_mannered? true end end

CSC/ECE 517 Lecture Notes © 2007 Edward F. Gehringer 8 jimmy = Person.new('Jimmy Olsen', 21, 'cub reporter') clark = Person.new('Clark Kent', 35, 'reporter') jimmy.mild_mannered? # => true clark.mild_mannered? # => true

But it happens that some Person objects are not as mild-mannered as they might appear. Some of them have super powers. module SuperPowers def fly 'Flying!' end

def leap(what) "Leaping #{what} in a single bound!" end

def mild_mannered? false end

def superhero_name 'Superman' end end

If we use include to mix the SuperPowers module into the Person class, it will give every person super powers. Some people are bound to misuse such power. Instead, we'll use extend to give super powers only to certain people: clark.extend(SuperPowers) puts clark.superhero_name puts clark.fly

Lecture 7 Object-Oriented Languages and Systems 9 puts clark.mild_mannered? puts jimmy.mild_mannered?

Automatically loading libraries on demand If there is a big library with multiple components, one would want to split it up so that users don't have to load the entire library into memory just to use part of it.

But one wouldn’t want to make the users explicitly require each part of the library they plan to use.

The solution is to split the big library into multiple files, and set up autoloading for the individual files by calling Kernel#autoload. The individual files will be loaded as they're referenced.

Suppose there is a library function.rb with two large modules:

# functions.rb module Decidable # … Many, many methods go here. end module Semidecidable # … Many, many methods go here. end

One can provide the same interface, but possibly save your users some memory, by splitting functions.rb into three files. The functions.rb file itself becomes a stub full of autoload calls:

# functions.rb autoload :Decidable, "decidable.rb" autoload :Semidecidable, "semidecidable.rb"

CSC/ECE 517 Lecture Notes © 2007 Edward F. Gehringer 10 The modules themselves go into the files mentioned in the new functions.rb:

# decidable.rb module Decidable # … Many, many methods go here. end # semidecidable.rb module Semidecidable # … Many, many methods go here. end

The following code will work if all the modules are in functions.rb, but it will also work if functions.rb only contains calls to autoload: require 'functions' Decidable.class # => Module # More use of the Decidable module follows…

When Decidable and Semidecidable have been split into autoloaded modules, that code only loads the Decidable module.

Memory is saved that would otherwise be used to contain the unused Semidecidable module.

Lecture 7 Object-Oriented Languages and Systems 11

Recommended publications