How to write a general math operator

Hello. Here’s a problem I cannot solve:
Out of fun I’m writing a library to play with numbers, specifically Rationals:

package loonies

final case class Rational(
    val numerator: Long,
    val denominator: Long)
    extends Number {
  require(denominator != 0, "The denominator cannot be zero")
  val value = (numerator, denominator)

  def +(rhs: Rational) =
    Rational((numerator * rhs.denominator + denominator * rhs.numerator), 
    (denominator * rhs.denominator))
  def +(rhs: Integer) =
    Rational(numerator + rhs.value._1 * denominator, denominator)

    override def toString: String = s"Q{${numerator}/$denominator}"
}

Integers:

package loonies

final case class Integer(val numero: Long) extends Number {
  val value = (numero, 1L)
  //val num = numero

  def +(rhs: Integer): Integer = new Integer(numero + rhs.numero)
  def +(rhs: Rational): Rational =
    Rational(value._1 * rhs.denominator + rhs.numerator, rhs.denominator)

  def -(rhs: Integer): Integer = new Integer(numero - rhs.numero)

  def *(rhs: Integer): Integer = new Integer(numero * rhs.numero)

  /**
    * This operator tests if `this` Integer '''divides''' `that`
    * Integer
    */
  def |(rhs: Integer): Boolean = rhs.numero match {
    case s if (s % numero == 0) => true
    case _                      => false
  }

  override def toString: String = s"Z{${numero}}"
}

and, finally, the Supertype Number:

package loonies

trait Number {
  import Number._
  def value: (Long, Long)

  /**
    * This operator should return the result of the division of two integers `a'
    * and `b` if `b` divides `a`, otherwise a Rational number with `a` as
    * numerator and `b` as denominator
    */
  def /(that: Number): Number = that match {
    case t: Integer =>
      this match {
        case s: Integer if t | s    => num(s.numero / t.numero)
        case s: Integer if !(t | s) => num(s.numero, t.numero)
      }
  }
}

object Number {
  def num(l: Long) = Integer(l)

  def num(
      n: Long,
      d: Long
    ) = new Rational(n, d)
}

Now, my focus is on the / operator of the latter class: given two Integers a and b, if b|a I’ll get another Integer c = a ÷ b otherwise a Rational with a as its numerator and b its denominator.

So if I use them I get:

scala> import loonies._
import loonies._

scala> val a = Integer(5)
a: loonies.Integer = Z{5}

scala> val b = Integer(15)
b: loonies.Integer = Z{15}

scala> val c = b / a
c: loonies.Number = Z{3}

scala> val q = a / b
q: loonies.Number = Q{5/15}

scala> c + q
           ^
       error: type mismatch;
        found   : loonies.Number
        required: String

scala> q + c
           ^
       error: type mismatch;
        found   : loonies.Number
        required: String

scala> val s = c + q
                   ^
       error: type mismatch;
        found   : loonies.Number
        required: String

So, it looks like that, even if Rationals and Integers have operators overloaded so to deal with each other, still, I would have to write nested methods to allow Number to tackle the situation.
Anyone with any idea on how to circumvent, if possible, the problem?
Thanks
Guido

The problem is, that your division method has to return Number, but all operations are only defined on the subclasses. As the compiler cannot know which of both subtypes c will be, it cannot decide which + function to call. Also, the signatures in both number types are incompatible (different return types).

A simple solution would be to add the signature for the operations you want to the Number trait, e.g.

trait Number {
  def +(rhs: Number): Number
  // ...
}

and then implement it on both subtypes via a pattern match, merging the currently two methods:

final case class Integer(numero: Long) extends Number {

  def +(rhs: Number): Number = rhs match {
    case Integer(num) => Integer(numero + num)
    case Rational(numer, denom) => Rational(value._1 * denom + numer, denom)
  }
  // ...
}

This has the downside, that if you add another Number subtype, a runtime error may occur when passing that to one of those operators. A common solution is to put the types into the same file and make Number a sealed trait, so that no new types can be defined outside that file and allowing the compiler to do exhaustiveness checks on pattern matches.

A completely different approach would be to use typeclasses instead of subtyping. You can have a look at the source of https://typelevel.org/spire/ for a math library using that approach (although it does not wrap the primitive numeric types, and so cannot change to rational automatically for dividing non-divisible integers)

1 Like

Thanks! It works.
Yeah, I was afraid I would have ended up descending the path of Type Classes, which, given my level of expertise, I’m afraid, will present a steep gradient.
Thanks again.

To be clear, you only need type classes if you want to add new operations to scala.Int etc. If you only work with your own types, inheritance is fine.

1 Like