5

Learning the Scala3 extension and CanEqual concepts, but finding difficulty in extending certain features of an Int.

In the following example I am easily able to add >= functionality to Int to compare it to a RationalNumber case class, but unable to modify the behavior of ==. (note 1~2 is the same as RationalNumber(1,2)).

The problem seems to be tied in with basic AnyVal types and how Scala passes off to Java to handle equals and ==.

case class RationalNumber(val n: Int, val d: Int):
  def >=(that:RationalNumber) = this.num * that.den >= that.num * this.den
  //... other comparisons hidden (note not using Ordered for clarity)
  private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)
  val sign = if (n<0 ^ d<0) -1 else 1
  private val (an, ad) = (math.abs(n), math.abs(d))
  val num = sign * (an / gcd(an, ad))
  val den = if(an == 0) 1 else ad / gcd(an, ad)

  override def equals (that: Any): Boolean =
    that match
      case t: RationalNumber => t.den == den && t.canEqual(this) && t.num == num
      case t: Int => equals(RationalNumber(t,1))
      case _ => false

  override lazy val toString = s"$num/$den"

object RationalNumber:
  def apply (r: Int): RationalNumber = new RationalNumber(r, 1)
  import scala.language.implicitConversions
  implicit def intToRat (i: Int): RationalNumber = i ~ 1
  given CanEqual[RationalNumber, Int] = CanEqual.derived
  given CanEqual[Int, RationalNumber] = CanEqual.derived
  extension (i: Int)
    def ~(that: Int) = new RationalNumber(i, that)
    def >=(that: RationalNumber) = i ~ 1 >= that
    def equals (that: AnyVal) : Boolean =
      println("this never runs")
      that match
        case t: RationalNumber => t.den == 1 && t.num == i
        case _ => i == that
    def ==(that: RationalNumber) =
      println ("this never runs")
      i~1 == that

object Main:
  @main def run =
    import RationalNumber._
    val one = 1 ~ 1
    val a = 1 == one // never runs extension ==
    val b = one == 1
    val c = 1 >= one
    val d = one >= 1
    val ans = (a,b,c,d) // (false, true, true, true)
    println(ans)
aldudalski
  • 63
  • 5

1 Answers1

6

Extension methods are tried only if a qualifying method of the same name does not already exist. Hence since at least the following qualifying == is already defined on Int

def ==(arg0: Any): Boolean

it will not call your extension. If you change the name to say === then it would work

def ===(that: RationalNumber)

You could force implicit conversion with type ascription (1: RationalNumber) == one if you want. (Implicit conversions are discouraged).


Try extending ScalaNumericConversions which in turn extends ScalaNumber

case class RationalNumber(val n: Int, val d: Int) extends ScalaNumericConversions {
  def intValue: Int = ???
  def longValue: Long = ???
  def floatValue: Float = ???
  def doubleValue: Double = ???
  def isWhole: Boolean = false
  def underlying = this
...
  override def equals (that: Any): Boolean = {
    that match {
      case t: RationalNumber => t.den == den && t.canEqual(this) && t.num == num
      case t: Int => equals(RationalNumber(t,1))
      case _ => false
    }
  }
}

so now Scala will eventually call BoxesRuntime#equalsNumNum

public static boolean equalsNumNum(java.lang.Number xn, java.lang.Number yn) {
...
  if ((yn instanceof ScalaNumber) && !(xn instanceof ScalaNumber))
    return yn.equals(xn);
  }
...

which note flips the order of arguments and hence will call RationalNumber#equals, so in effect

1 == one

becomes

one.equals(1)

Found this approach by looking at the :javap - in REPL for 1 == BigInt(1)

30: invokestatic  #54  // Method scala/runtime/BoxesRunTime.equals:(Ljava/lang/Object;Ljava/lang/Object;)Z

and then following trail laid out by BoxesRunTime.equals

Mario Galic
  • 47,285
  • 6
  • 56
  • 98
  • By that argument def >= already exists and shouldn't work. But in fact it does. I'm looking for why == works differently. I can provide extension definitions for other methods of Int but not for == and !=. – aldudalski Jun 26 '21 at 23:11
  • @aldudalski there is no `>=` method taking a `RationalNumber` on the `Int` class. However `==` takes an `Any` as such it will always match. – Luis Miguel Mejía Suárez Jun 27 '21 at 02:09
  • 1
    @LuisMiguelMejíaSuárez thanks for the clarificationI was only seeing Byte, Short, Char, Int, Long, Float, Double definitions. The trick of course is to go through AnyVal to Any where the def==(argo: Any):Boolean is hiding. I guess I have to go the === route that Mario suggested. – aldudalski Jun 27 '21 at 18:19
  • @aldudalski yeah is annoying but getting rid of universal equality would be way painful. Anyways a custom triple equals operator is very common in **Scala** there are at least five libraries providing their own lol. – Luis Miguel Mejía Suárez Jun 27 '21 at 18:43
  • 1
    Follow up thought: BigInt(1) == 1 and 1 == BigInt (1). BigInt is not explicitly described in Int but overcomes this problem – aldudalski Jun 27 '21 at 20:09
  • thanks for the updates in your answer good find using javap – aldudalski Jul 08 '21 at 04:33
  • @mariogalic thanks for the :javap check. That explains it. – aldudalski Jul 19 '21 at 20:24