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:
:
, the reciever is the thing on the rightMy 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.
I tried adding infix
keyword before def
, but it doesn't change what is printed in this example.
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)"