CS 214: Programming Languages
Spring 2009

Home|Syllabus|Schedule
<<|>>

Mixins
Ruby, Iteration 5

The Problem: Multiple Inheritance

One of the smartest things that the Java (and C#) inventors did was to not allow multiple inheritance. It's not that useful, and it typically causes more problems than it fixes. There are a variety of reasons for this which we'll see in a different iteration (and maybe a different context).

Ruby does allow multiple inheritance, but in a strange way. Technically, you can extend only one class (just as in Java and C#), but you can mix in a module to gain extra behaviors.

Remember: OOP is more about behaviors than about objects.

Normal inheritance looks like this:

class Integer < Numeric
   ...
end

Integer is said to extend the Person class. Everything Numeric has is automatically given to Integer, including instance variables.

Modules and mixins work a little differently. Modules can be used for other reasons (mostly for creating namespaces), but when they define instance methods, they can be included in a class and those instance methods are mixed in with the original methods of the class.

Ruby provides a few incredibly useful modules. We'll look at the Comparable module.

The Comparable Module

Typically there are seven comparison methods that Ruby lets you define. First, Ruby has the standard comparisons (e.g., <, <=, ==, >=, >) from basic mathematics: 4 < 5 and 8 >= -4.

Second, Ruby has a between? method: 3.between?(1, 5) (which is true) and 6.between?(1, 5) (which is false).

The <=> operator is affectionally referred to as "the spaceship operator".

Third, Ruby has a ternary comparison (similar to compareTo(T) in Java and strcmp(s1,s2) in C/C++): 1 <=> 3 returns -1, 1 <=> 1 returns 0, 3 <=> 1 returns +1. (Other languages say that the results will be less than zero, zero, or greater than zero, but Ruby seems to insist on returning -1, 0, or +1. However, don't count on it.)

You could write definitions for all seven methods yourself. You could write them in terms of the others making some of the computations easier, but there would still be seven definitions.

You could write a superclass and use normal inheritance, but then you use up your one official superclass.

Keep in mind that these seven methods are all behavior.

What if you only had to define just one of these methods and let a module take care of the rest?

Running Time

To keep track of the running time of different programs, I write this class:

running_time.rb

class RunningTime
  include Comparable
  def initialize(name, running_time)
    @name = name
    @running_time = running_time
  end
  def running_time
    @running_time
  end
  def <=>(other)
    running_time <=> other.running_time
  end
end

Note the highlighted line of code.

Create a new folder ruby/running_time. Create the running_time.rb file with these contents. Create a running_time_test.rb.

running_time_test.rb

class RunningTimeTest < Test::Unit::TestCase
  def setup
    @pokey = RunningTime.new("Pokey", 12)
    @speedy = RunningTime.new("Speedy", 3)
  end
  def test_ternary
    assert_equal +1, @pokey <=> @speedy
    assert_equal  0, @pokey <=> @pokey
    assert_equal  0, @speedy <=> @speedy
    assert_equal -1, @speedy <=> @pokey
  end
  def test_less_than
    assert  (@speedy < @pokey)
    assert !(@pokey < @pokey)
    assert !(@pokey < @speedy)
  end
end

RunningTime is a class for keeping track of running time (for cross country, swim meets, program analysis, etc.). Instead of defining all seven methods for comparisons, I define only <=> because all of the others can be defined in terms of it.

In order to get the other six methods, I included the Comparable module with include Comparable. This is not the same as a C/C++ #include (which is evil and despised by everyone). Ruby's include mixes in all of the instance methods from the named module, as if they were defined in the class itself. It's actually a bit more efficient and flexible than that. (Efficient: it's not a textual include, it's virtual. Flexible: since it's virtual, changes to the module are virtually updated for everyone and everything.)

The real key is that the RunningTime objects get all those methods for free.

Java and C# don't allow you to do this; you'll typically have to reimplement those methods or use adapter pattern (which only reduces the amount of thinking you have to do, not the number of methods you have to write). C++ allows you to inherit from multiple classes, and it can turn into a right terrible mess without too much trying.

Your Turn

Create a new folder ruby/name, a new file name.rb with a Name class, and a new file name_test.rb with a test-case class. Write tests and computations based on the description below.

The Name class should keep track of first name and last name separately. (There should be Name#first_name and Name#last_name methods.) Using the Comparable module, implement the seven comparison methods so that names are ordered properly—compare last names first; if equal, use the first name.

Name.new('Lee', 'Adama') is less than Name.new('Kara', 'Thrace') (because 'Adama' is less than 'Thrace'). Name.new('Sarah Jane', 'Smith') is greater than Name.new('Dr. Zachary', 'Smith') (because 'Smith' equals 'Smith' and 'Sarah Jane' is greater than 'Dr. Zachary'). Since you have two things to consider, it makes your <=> a little more interesting. But it doesn't make the other methods any harder!

Work hard on some good test cases.