How to write a general math operator

#1

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

#2

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
#3

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.

#4

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