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 namedhello
, the compiler looks for an implicit class whose constructor takes aString
parameter and has ahello
method -
In this example, the compiler finds
BetterString
, which takes aString
parameter and has a method namedhello
-
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.
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |
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