This is an excerpt from the 1st Edition of the Scala Cookbook (partially modified for the internet). This is Recipe 1.12, “How to Add Your Own Methods to the String Class.”
SCALA 3 UPDATE: Please note that this approach works with Scala 2. The technique is different in Scala 3. I describe the Scala 3 approach in Using Term Inference with Given and Using, and also in, A complete Dotty (Scala 3) “given” example.
Scala FAQ: Can you share an example of how to create an implicit class in Scala 2.10 (and newer)?
Sure. As the question implies, the implicit class functionality changed in Scala 2.10, so let's take a look at the new syntax.
Background
Rather than create a separate library of String
utility methods, like a StringUtilities
class, you want to add your own behavior(s) to the String
class. This will let you write code like this:
"HAL".increment
instead of this:
StringUtilities.increment("HAL")
Solution
In Scala 2.10, you define an implicit class, and then define methods within that class to implement the behavior you want.
You can see this in the REPL. First, define your implicit class and method(s):
scala> implicit class StringImprovements(s: String) { | def increment = s.map(c => (c + 1).toChar) | } defined class StringImprovements
Once this is done you can invoke your increment
method on any String
:
scala> val result = "HAL".increment result: String = IBM
In real-world code, this is just slightly more complicated. According to SIP-13, Implicit Classes, “An implicit class must be defined in a scope where method definitions are allowed (not at the top level).” This means that your implicit class must be defined in one of these places:
- A class
- An object
- A package object
Put the implicit class in an object
One way to satisfy this condition is to put the implicit class inside an object. For instance, you can place the StringImprovements
implicit class in an object such as a StringUtils
object, as shown here:
package com.alvinalexander.utils object StringUtils { implicit class StringImprovements(val s: String) { def increment = s.map(c => (c + 1).toChar) } }
You can then use the increment
method somewhere else in your code, after adding the proper import
statement:
package foo.bar import com.alvinalexander.utils.StringUtils._ object Main extends App { println("HAL".increment) }
Put the implicit class in a package object
Another way to satisfy the requirement is to put the implicit class in a package object. With this approach, place the following code in a file named package.scala, in the appropriate directory. If you’re using SBT, you should place the file in the src/main/scala/com/alvinalexander directory of your project, containing the following code:
package com.alvinalexander package object utils { implicit class StringImprovements(val s: String) { def increment = s.map(c => (c + 1).toChar) } }
When you need to use the increment
method in some other code, use a slightly different import statement from the previous example:
package foo.bar import com.alvinalexander.utils._ object MainDriver extends App { println("HAL".increment) }
See Recipe 6.7 of the Scala Cookbook, “Putting Common Code in Package Objects,” for more information about package objects.
Using versions of Scala prior to version 2.10
If for some reason you need to use a version of Scala prior to version 2.10, you’ll need to take a slightly different approach to solve this problem. In this case, define a method named increment
in a normal Scala class:
class StringImprovements(val s: String) { def increment = s.map(c => (c + 1).toChar) }
Next, define another method to handle the implicit conversion:
implicit def stringToString(s: String) = new StringImprovements(s)
The String
parameter in the stringToString
method essentially links the String
class to the StringImprovements
class.
Now you can use increment
as in the earlier examples:
"HAL".increment
Here’s what this looks like in the REPL:
scala> class StringImprovements(val s: String) { | def increment = s.map(c => (c + 1).toChar) | } defined class StringImprovements scala> implicit def stringToString(s: String) = new StringImprovements(s) stringToString: (s: String)StringImprovements scala> "HAL".increment res0: String = IBM
Discussion
As you just saw, in Scala, you can add new functionality to closed classes by writing implicit conversions and bringing them into scope when you need them. A major benefit of this approach is that you don’t have to extend existing classes to add the new functionality, like you would have to do in a more restricted OOP language.
For instance, there’s no need to create a new class named MyString
that extends String
, and then use MyString
throughout your code instead of String
; instead, you define the behavior you want, and then add that behavior to all String
objects in the current scope when you add the import
statement.
Note that you can define as many methods as you need in your implicit class. The following code shows both increment
and decrement
methods, along with a method named hideAll
that returns a String
with all characters replaced by the *
character:
implicit class StringImprovements(val s: String) { def increment = s.map(c => (c + 1).toChar) def decrement = s.map(c => (c − 1).toChar) def hideAll = s.replaceAll(".", "*") }
Notice that except for the implicit
keyword before the class name, the StringImprovements
class and its methods are written as usual. By simply bringing the code into scope with an import
statement, you can use these methods, as shown here in the REPL:
scala> "HAL".increment res0: String = IBM
Here’s a simplified description of how this works:
- The compiler sees a string literal
HAL
. - The compiler sees that you’re attempting to invoke a method named
increment
on theString
. - Because the compiler can’t find that method on the
String
class, it begins looking around for implicit conversion methods that are in scope that accept aString
argument. - This leads the compiler to the
StringImprovements
class, where it finds theincrement
method.
That’s an oversimplification of what happens, but it gives you the general idea of how implicit conversions work.
For more details on what’s happening here, see SIP-13, Implicit Classes.
Annotate your method return type
It’s recommended that the return type of implicit method definitions should be annotated. If you run into a situation where the compiler can’t find your implicit methods, or you just want to be explicit when declaring your methods, add the return type to your method definitions.
In the increment
, decrement
, and hideAll
methods shown here, the return type of String
is made explicit:
implicit class StringImprovements(val s: String) { // being explicit that each method returns a String def increment: String = s.map(c => (c + 1).toChar) def decrement: String = s.map(c => (c − 1).toChar) def hideAll: String = s.replaceAll(".", "*") }
Returning other types
Although all of the methods shown so far have returned a String
, you can return any type from your methods that you need. The following class demonstrates several different types of string conversion methods:
implicit class StringImprovements(val s: String) { def increment = s.map(c => (c + 1).toChar) def decrement = 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 } }
With these new methods you can now perform Int
and Boolean
conversions, in addition to the String
conversions shown earlier:
scala> "4".plusOne res0: Int = 5 scala> "0".asBoolean res1: Boolean = false scala> "1".asBoolean res2: Boolean = true
Note that all of these methods have been simplified to keep them short and readable. In the real world, you’ll want to add some error-checking.
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |