Learn myself some Scala 3, episode 2: extension methods

Extension methods have always – or at least as long as I have known Scala – been around. Before Scala 2.10 they had to be provided by a rather verbose pattern:

1
2
3
4
5
6
7
8
9
10
object IntSyntax {

final class IntOps(n: Int) {
def reverse: Int =
n.toString.reverse.toInt
}

implicit def toIntOps(n: Int): IntOps =
new IntOps(n)
}

If these two definitions – a wrapper class providing the actual extension method and an implicit conversion from the type to be extended to the wrapper – are in scope, we can call the extension method:

1
2
3
4
5
6
7
8
9
scala> 123.reverse
^
error: value reverse is not a member of Int

scala> import IntSyntax._
import IntSyntax._

scala> 123.reverse
res1: Int = 321

As extension methods are a frequently used feature in Scala, the above pattern was simplified in Scala 2.10 by the introduction of implicit classes:

1
2
3
4
5
6
7
object IntSyntax {

final implicit class IntOps(val n: Int) extends AnyVal {
def reverse: Int =
n.toString.reverse.toInt
}
}

So by adding implicit to the definition of the wrapper class we can omit the definition of the implicit conversion which will be added by the compiler for us. Notice that we extend AnyVal just for performance reasons, i.e. in order to avoid allocating the wrapper object. But we still have to write too much boilerplate code and we also conflate or overload the concept of “implicits”.

Scala 3 completely replaces the implicit keyword and its multiple overloaded applications with a couple of contextual abstractions. One of these are extension methods which are supported via new syntax instead of a pattern:

1
2
def (n: Int) reverse: Int =
n.toString.reverse.toInt

Extension methods are defined with an additional parameter list which takes exactly one parameter and is placed in front of the method name. If they are in scope as a simple identifier, e.g. via importing, they can be appied at call site using the dot notation:

1
2
scala> 123.reverse
val res0: Int = 321

As they are translated to ordinary methods with the additional parameter list inserted as first one, they could also be called accordingly:

1
2
scala> reverse(123)
val res1: Int = 321

Of course extension methods can also be polymorphic:

1
2
3
4
5
def [A](lhs: A) |+| (rhs: A): A =
lhs.combine(rhs)

def [A](lhs: A) combine(rhs: A): A =
???

The astute reader probably guesses, that in order to implement these extension methods we need type classes – semi group in this case – which we will cover in the next episode.