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
3
extension (n: Int)
def reverse: Int =
n.toString.reverse.toInt

Extension methods are defined using the new soft keyword extension and an additional parameter list taking the receiver to be extended. 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

Of course extension methods can also be collective – i.e. more than one for a single receiver – and polymorphic:

1
2
3
4
5
6
extension (lhs: A)
def combine(rhs: A): A =
???

final def |+|(rhs: A): A =
lhs.combine(rhs)

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.