user1934428
user1934428

Reputation: 22237

How to implement 'Comparable' if parent class already has a '=='

I have a library class, which provides methods for equality and inequality. I'm deriving another class from this, which, differently from the parent class, introduces an ordering relation, i.e. it would make sense to ask for two elements of the derived class, which one is smaller. In particular, arrays of objects of the derived class can be sorted.

My first approach was

class MyClass < LibraryClass
  def <(other)
    ...
  end
  def <=>(other)
    return 0 if self == other
    return -1 if self < other
    return 1
  end
  # code for operators > <= >= is not shown here....
end

This seems to work, but I thought that it may be better to instead [sic] Comparable, since this would give plenty of other methods for free.

  1. The description of Comparable says that I have to implement the <=> operator, and other operators including == and != would then be automatically implemented. However, I am already happy with the == operator from the parent class, so no new method for equality should be generated.

  2. I want to test for equality using the == operator from the parent class. If I implement the <=> operator, and Comparable implements an == operator in terms of my <=> operator, I would end up in a recursive call.

For expression self == other, how can I specify that the == operator of the parent class should be invoked?

Upvotes: 2

Views: 73

Answers (2)

Cary Swoveland
Cary Swoveland

Reputation: 110685

First, let's create a subclass of Range that overwrites Range#==.

class OddRange < Range
  def ==(other)
    !super
  end
end

OddRange.new(1, 10) == OddRange.new(2, 7)
  #=> true

Note that

OddRange.included_modules
  #=> [Enumerable, Kernel]

which does not include Comparable. Now let's create a subclass of OddRange and examine it's behaviour.

class MyRange < OddRange
end

MyRange.ancestors
  #=> [OddRange, Range, Enumerable, Object, Kernel, BasicObject]
MyRange.instance_method(:==).owner
  #=> OddRange 
rng1 = MyRange.new(1, 5)
rng2 = MyRange.new(2, 4)
rng3 = MyRange.new(1, 5)
rng1 == rng2
  #=> true 
rng1 == rng3
  #=> false

We next include the module Comparable into MyRange, and add an instance method to MyRange1.

class MyRange
  def <=>(other)
    self.end <=> other.end
  end
  include Comparable
end

MyRange.ancestors
  #=> [MyRange, MyComparable, OddRange, Range, Enumerable, Object, Kernel, BasicObject]
MyRange.instance_method(:==).owner
  #=> Comparable 

rng1 = MyRange.new(1, 5)
rng2 = MyRange.new(2, 4)
rng3 = MyRange.new(1, 5)
rng1 == rng2
  #=> false
rng1 == rng3
  #=> true
rng1 <=> rng2
  #=> 1

No surprises.

If we don't want Comparable#== to overwrite MyRange#==, we could do the following.

class MyRange
  def ==(other)
    method(__method__).super_method.super_method.call(other)  
  end
end

This "jumps over" Comparable and uses OddRange's method :==. See Method#super_method.

rng1 = MyRange.new(1, 5)
rng2 = MyRange.new(2, 4)
rng3 = MyRange.new(1, 5)
rng1 == rng2
  #=> true
rng1 == rng3
  #=> false

Now let's add another instance method to OddRange.

class OddRange
  def :<=(other)
    (self.begin <=> self.begin) <= 0
  end
end

rng1 = MyRange.new(1, 5)
rng2 = MyRange.new(2, 4)
rng1 <= rng2
  #=> false

We see that Comparable#<= and MyRange#<> cause rng1 <= rng2 to return false. If we wished to instead use OddRange#<=, we could of course do what we did before, add

 def <=(other)
    method(__method__).super_method.super_method.call(other)  
 end

to MyRange. More generally, if we don't want Comparable instance methods to overwrite any OddRange instance methods (which may change over time), we could do the following.

class MyRange
  (instance_methods & OddRange.instance_methods(false)).each do |m|
    define_method(m) do |other|
      method(__method__).super_method.super_method.call(other)  
    end
  end
end

rng1 = MyRange.new(1, 5)
rng2 = MyRange.new(2, 4)
rng3 = MyRange.new(1, 5)
rng1 == rng2
  #=> true
rng1 == rng3
  #=> false
rng1 <= rng3
  #=> true

1 Redefining rng1, rng2 and rng3 below isn't actually necessary.

Upvotes: 1

J&#246;rg W Mittag
J&#246;rg W Mittag

Reputation: 369478

The include makes Comparable the superclass of MyClass and LibraryClass the superclass of Comparable. So, the implementation of == in Comparable overrides the implementation of == in LibraryClass.

What you can then do, is to override == again in MyClass with a version that is identical to the one in LibraryClass:

class MyClass < LibraryClass
  include Comparable

  def <=>(other)
    # whatever
  end

  define_method(:==, LibraryClass.public_instance_method(:==))
end

Upvotes: 2

Related Questions