2

When I manually converted Scala 2 code to Scala 3, operator precedence for my DSL changed, and it took me a long time to debug and fix. It seems the handling of : is different:

      extension (i1: Int) def ~>:(i2: Int) = i1 < i2
      extension (i1: Int) def ~>(i2: Int) = i1 < i2

      class Wrap(val i: Int):
        def ~>:(w: Wrap) = i ~>: w.i
        def ~>(w: Wrap) = i ~> w.i

      // `Wrap` preserves `~>`
      println(1 ~> 2) // true
      println(Wrap(1) ~> Wrap(2)) // true

      // `Wrap` does not preserve `~>:`
      println(1 ~>: 2) // true
      println(Wrap(1) ~>: Wrap(2)) // false

My mental model was:

  • For methods ending in :, the reciever is the thing on the right
  • extension methods are just methods: it's as if the method is added to the class

My mental model seems to be wrong. What's the right way of explaining what's happening?

Links would help, I checked the Scala 3 docs and didn't find anything about how custom operators associate.

Update

I tried adding infix keyword before def, but it doesn't change what is printed in this example.

halfer
  • 19,824
  • 17
  • 99
  • 186
Max Heiber
  • 14,346
  • 12
  • 59
  • 97
  • 2
    I have the impression that there is indeed a problem, but your specific example is very confusing, because of all the integers on both sides, and comparisons and left-right, and two nesting levels and what not. Here is an example that simply produces a compilation error: `class A(val i: Int) { infix def >>:(s: String) = s + ">>:" + i }; extension (h: A) { infix def >+:(s: String) = s + ">+:" + h.i }; println("x" >>: A(42)); println("y" >+: A(58));`, without any arithmetic and less-than-greater-than confusions. – Andrey Tyukin Apr 18 '21 at 14:55
  • @AndreyTyukin "simple" can be subjective - thank you for this additional example. Also - I'm not saying Scala is doing the wrong thing, just that I need help forming a coherent mental model of what it's doing and a link to a good reference – Max Heiber Apr 18 '21 at 16:27

1 Answers1

1

Your mental model just needs some tweaking.

Recall that the infix x op y de-sugars to x.op(y), except when the op ends with a colon, then it's y.op:(x). This holds true whether the op() method is native to the instance parameter or an added extension, which, in Scala-2, is handled by an intermediate implicit class.

implicit class IntermediateClass(instance: Int) {
  def op(arg: Int) = ???
}

A Scala-3 extension, on the other hand, is just a method that receives two curried arguments. So the infix invocation leftOfOp op rightOfOp will always be handled as such:

extension (leftOfOp: Int)
  def op(rightOfOp: Int) = ???

And it's the same whether op has a trailing : or not. But, while the code at the definition site remains consistent in this manner, the associativity at the call site is as you would expect.

extension (left: String)
  def @:(right: String):String = s"$left.@:($right)"
  def @@(right: String):String = s"$left.@@($right)"

"TOP" @: "MID" @: "END"  //"TOP.@:(MID.@:(END))"
"top" @@ "mid" @@ "end"  //"top.@@(mid).@@(end)"

More details can be found here and here.

jwvh
  • 50,871
  • 7
  • 38
  • 64