Search code examples
scalaoperator-overloadingscala-compiler

What may have caused the following operator overloading to swap operands?


I'm using scala language to define 2 operators: :++ and ++: that serves as the exact mirror of each other: a :++ b == b ++: a, they are obviously not commutative: a :++ b != a ++: b.

This is my scala code for testing:

import org.scalatest.FunSpec

import scala.collection.immutable.ListMap

case class Example(self: ListMap[String, String] = ListMap.empty) {

  def :++(v: Example) = this.copy(
    self ++ (v.self -- self.keys.toSeq)
  )

  def ++:(v: Example) = {
    println("forward: " + :++(v))
    println("reverse: " + (v :++ this))
    v :++ this
  }
}

class OperatorOverrideSuite extends FunSpec {

  val p1 = Example(ListMap("a" -> "1"))
  val p2 = Example(ListMap("a" -> "2"))

  it(":++ operator should preserve first value") {
    assert(p1 :++ p2 == p1)
  }

  it("++: operator should preserve second value") {
    assert(p1 ++: p2 == p2)
  }
}

The first test looks all good, but when running the I got the following error:

forward: Example(Map(a -> 2))
reverse: Example(Map(a -> 1))

Example(Map(a -> 1)) did not equal Example(Map(a -> 2))
ScalaTestFailureLocation: com.schedule1.datapassports.params.OperatorOverrideSuite$$anonfun$2 at (OperatorOverrideSuite.scala:30)
Expected :Example(Map(a -> 2))
Actual   :Example(Map(a -> 1))
 <Click to see difference>

org.scalatest.exceptions.TestFailedException: Example(Map(a -> 1)) did not equal Example(Map(a -> 2))
    at org.scalatest.Assertions$class.newAssertionFailedException(Assertions.scala:528)
    at org.scalatest.FunSpec.newAssertionFailedException(FunSpec.scala:1630)
    at org.scalatest.Assertions$AssertionsHelper.macroAssert(Assertions.scala:501)
    at ...

from the printed message it appears that scala override my operator and reverse the operands on its own, what may caused scala compiler to behave this way? Is it a bug?

I'm using the latest scala 2.11 and latest Java 8u181 for testing.


Solution

  • From Scala's language specification:

    The associativity of an operator is determined by the operator's last character. Operators ending in a colon `:' are right-associative. All other operators are left-associative.

    Here's a demonstration of that:

    scala> case class Test(name: String) {
         |     def ++:(that: Test) = println(s"Called ++: on $this with argument $that")
         |     def :++(that: Test) = println(s"Called :++ on $this with argument $that")
         | }
    defined class Test
    
    scala> val (x, y) = (Test("x"), Test("y"))
    x: Test = Test(x)
    y: Test = Test(y)
    
    scala> x ++: y
    Called ++: on Test(y) with argument Test(x)
    
    scala> x :++ y
    Called :++ on Test(x) with argument Test(y)
    

    As a result, when you say p1 ++: p2 in your code, what gets executed is p2.++:(p1), which is equivalent to p1 :++ p2. This means that your two operators are actually strictly equivalent.