Asone Tuhid
Asone Tuhid

Reputation: 549

Ruby: overload operator behaviour for some cases only

My question is: how do I overload an operator on a builtin class (such as Integer.new.+) but only for some cases, depending on the class of the second operand

This is the behaviour I'm looking for:

myObject = myClass.new
1 + myObject #=> special behaviour
1 + 2        #=> default behaviour (3)

For example, in Python I would define a __radd__ method on myClass to override case 1.

I've tried using super but apparently Numeric doesn't have operator methods.

Ideally, what I'm looking for is a way to extract the + method and rename it.

Like this:

class Integer
  self.plus = self.+  # you know what i mean, I don't know how else to express this.
                      # I also know that methods don't work like this, this is just to
                      # illustrate a point.
  def + other
    other.class == myClass ? special behaviour : self.plus other
  end
end

Thanks for your help

Upvotes: 4

Views: 493

Answers (2)

fylooi
fylooi

Reputation: 3870

Yes, you can override the behavior of almost anything in the standard library to achieve an outcome, but that's going to hurt understanding of the code and come back to bite you sometime in the future.

In this particular case, Fixnum#+ is designed to take a numeric value and return a numeric result. If we want to define our own classes to interact with Fixnum#+, we need to understand the design contract and adhere to it.

The general convention in Ruby is to use duck typing. We don't care about the class of the object, we just care whether it behaves like / can be converted to the object we want. Eg:

class StringifiedNumber
  def initialize(number)
    @number = number
  end

  # String#+ calls to_str on any object passed to it

  def to_str
    # replace with number to string parsing logic
    "one hundred"
  end
end 

> "total: " + StringifiedNumber.new(100)
=> "total: one hundred"

Things are a bit more complex with numbers since you may mix integers, floats, complex numbers, etc. The convention to handle this is to define a coerce method which returns two elements of the same type which are then used to perform the requested operation.

class NumberfiedString
  def initialize(string)
    @string = string
  end

  def to_i
    # replace with complicated natural language parsing logic
    100
  end

  def +(other_numberfied_string)
    NumberfiedString.new(self.to_i + other_numberfied_string.to_i)
  end

  # For types which are not directly supported,
  # Fixnum#+(target) will call the equivalent of 
  # target.coerce[0] + target.coerce[1] 

  def coerce(other)
    [NumberfiedString.new(other.to_s), self]
  end
end

> NumberfiedString.new("one hundred") + NumberfiedString.new("one hundred")
=> #<NumberfiedString:0x007fadbc036d28 @string=200>

> 100 + NumberfiedString.new("one hundred") 
=> #<NumberfiedString:0x007fadbc824c88 @string="200">

To answer OP's follow up question:

Is there no equivalent to Python's radd and related methods? (Where, if the first operand doesn't support the operation or the types, the second operand takes over)

class MyClass
  def +(other)
    puts "called +"
  end

  def coerce(other)
    [self, other]
  end
end

> 1 + MyClass.new
called +
=> nil

Upvotes: 3

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 120990

Both approaches posted here so far are a legacy Rails way, which is plain wrong. It relies on the fact that the class has no method called plus and nobody will reopen the class to create a method called plus. Otherwise things will go mad.

The correct solution is Module#prepend:

Integer.prepend(Module.new do
  def + other
    case other
    when Fixnum then special_behaviour
    else super(other)
    end
  end
end)

Upvotes: 6

Related Questions