Implicit methods/functions in Scala 2 and 3 (Dotty extension methods)

Scala lets you add new methods to existing classes that you don’t have the source code for, i.e., classes like String, Int, etc. For instance, you can add a method named hello to the String class so you can write code like this:

"joe".hello

which yields output like this:

"Hello, Joe"

Admittedly that’s not the most exciting method in the world, but it demonstrates the end result: You can add methods to a closed class like String. Properly (tastefully) used, you can create some really nice APIs.

In this article I’ll show how you can create implicit methods (also known as extension methods) in Scala 2 and Scala 3 (Dotty).

Scala 2: Create the method in an implicit class

Ever since Scala 2.10 you’ve been able to add a method to an existing, closed class by creating something known as an implicit class. This code demonstrates the approach:

import scala.language.implicitConversions

implicit class BetterString(val s: String) {
    def hello: String = s"Hello, ${s.capitalize}"
}

Now, when BetterString is in scope, you can use the hello method on a string. For instance, if you paste that code into the REPL you’ll see that you can use hello like this:

scala> "al".hello
res0: String = Hello, Al

If you’re interested in how implicit classes work, the process is described in detail in the book, Programming in Scala (Third Edition). One good quote from that book states that for implicit classes, “the compiler generates an implicit conversion from the class’s constructor parameter (such as String) to the class itself.” The book also describes the compiler process, which I’ll summarize:

  • Since String has no method named hello, the compiler looks for an implicit class whose constructor takes a String parameter and has a hello method

  • In this example, the compiler finds BetterString, which takes a String parameter and has a method named hello

  • The compiler inserts a call to this conversion, then does all the usual type-checking stuff

Add many implicit methods!

A great thing about the implicit class approach is that not only can you use it to define one new method for the String class, you can use it to add many new methods at one time:

implicit class StringImprovements(val s: String) {
    def increment = s.map(c => (c + 1).toChar)
    def hideAll: String = s.replaceAll(".", "*")
    def plusOne = s.toInt + 1
    def asBoolean = s match {
        case "0" | "zero" | "" | " " => false
        case _ => true
    }
}

In this example the StringImprovements class adds four new methods to the String class. Here are examples of the methods:

"HAL".increment      // IBM
"password".hideAll   // ********
"4".plusOne          // Int = 5
"0".asBoolean        // Boolean = false
"1".asBoolean        // Boolean = true

Scala 2 implicit class rules

According Programming in Scala (Third Edition) there are a few rules about implicit classes:

  • An implicit class constructor must have exactly one parameter

  • Must be located in an object, class, or trait

  • An implicit class can’t be a case class

As a practical matter that means writing code like this:

package com.alvinalexander.utils

object StringUtils {
    implicit class StringImprovements(val s: String) {
        def increment = s.map(c => (c + 1).toChar)
    }
}

and then using an import statement when you want to use the implicit methods:

import com.alvinalexander.utils.StringUtils.StringImprovements

println("HAL".increment)

Scala 3 (Dotty): Adding methods to closed classes with extension methods

That same approach will continue to work with Scala 3, but you’ll also be able to take advantage of a new technique to achieve the same result, and I think the syntax is a little more direct and obvious.

With Dotty you can create a construct formally called an “extension method” — some people used this term in Scala 2, but it was unofficial — to accomplish the same thing I just showed. An advantage of the extension method syntax is that there’s a little less boilerplate code required to achieve the same effect.

Here’s the hello method written as a Dotty extension method:

extension (s: String)
   def hello: String = s"Hello, ${s.capitalize}"

When you paste that code into the Dotty REPL (started with dotr) you’ll see that hello works just like it did before:

scala> "world".hello
val res0: String = Hello, World

The Scala 3 extension method syntax

With the Scala 3 extension method syntax you start with the extension keyword and the type you want to add one or more methods to. In this case I want to add a method to the String type:

extension (s: String)
           ---------

Then you follow that code by the method name you want to create:

def hello: String = s"Hello, ${s.capitalize}"
    -----

Because hello doesn’t take any parameters I don’t declare any, but I’ll show an example in a few moments that takes a parameter. Next, you declare the function’s return type as usual:

def hello: String = s"Hello, ${s.capitalize}"
           ------

And then the method body as usual:

def hello: String = s"Hello, ${s.capitalize}"
                    -------------------------

So the big change here is that you declare the type that the function will be added to first.

A Dotty extension method that takes a parameter

NOTE: As of late January, 2021, this section needs to be updated for the last Scala 3 extension method syntax.

To build on what you just saw, here’s a Dotty extension method that I add to the String class that takes an additional parameter:

def (s: String) makeInt(radix: Int): Int = Integer.parseInt(s, radix)

As before, this part of the code declares that I’m adding a method to the String class:

def (s: String) makeInt(radix: Int): Int = Integer.parseInt(s, radix)
     ---------

After that, the rest of the code looks like a normal Scala method named makeInt. It takes an Int parameter named radix:

def (s: String) makeInt(radix: Int): Int = Integer.parseInt(s, radix)
                        ----------

and returns an Int:

def (s: String) makeInt(radix: Int): Int = Integer.parseInt(s, radix)
                                     ---

Also note that both of the parameters s and radix are used in the method body:

def (s: String) makeInt(radix: Int): Int = Integer.parseInt(s, radix)
                                                            -  -----

This is how makeInt works:

"1".makeInt(2)      // Int = 1
"10".makeInt(2)     // Int = 2
"100".makeInt(2)    // Int = 4

"1".makeInt(8)      // Int = 1
"10".makeInt(8)     // Int = 8
"100".makeInt(8)    // Int = 64

"1".makeInt(10)     // Int = 1
"10".makeInt(10)    // Int = 10
"100".makeInt(10)   // Int = 100

Discussion (Dotty syntax)

When I first saw the Dotty extension method syntax I didn’t like it — I thought the Kotlin syntax was cleaner — but once I wrote a couple of methods I began to understand its logic. A key thing that it gives you is that you can assign a variable name to the type you’re adding the method to. In my examples I use the variable name s when adding methods to a String, but in more complicated examples you can use more complicated variable names.

Where to put Dotty extension methods

In the real world — i.e., life outside the REPL — you’ll either want to put extension methods in (a) the same class or object you’re using them, or (b) in an object like this:

object StringEnhancements {
    def (s: String) hello: String = s"Hello, ${s.capitalize}"
    def (s: String) makeInt(radix: Int): Int = Integer.parseInt(s, radix)
}

Once they’re in an object you can import them into your current scope as usual:

import StringEnhancements._

"al".hello         // String = Hello, Al
"100".makeInt(2)   // Int = 4