A Scala 2.10 implicit class example (how to add new functionality to closed classes)

This is an excerpt from the Scala Cookbook (partially modified for the internet). This is Recipe 1.12, “How to Add Your Own Methods to the String Class.”

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.

Back to top

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
Back to top

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.

Back to top

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
Back to top

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:

  1. The compiler sees a string literal HAL.
  2. The compiler sees that you’re attempting to invoke a method named increment on the String.
  3. 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 a String argument.
  4. This leads the compiler to the StringImprovements class, where it finds the increment 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.

Back to top

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(".", "*")
}
Back to top

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.

Back to top

The Scala Cookbook

This tutorial is sponsored by the Scala Cookbook, which I wrote for O’Reilly:

You can find the Scala Cookbook at these locations:

Back to top

Add new comment

The content of this field is kept private and will not be shown publicly.

Anonymous format

  • Allowed HTML tags: <em> <strong> <cite> <code> <ul type> <ol start type> <li> <pre>
  • Lines and paragraphs break automatically.
By submitting this form, you accept the Mollom privacy policy.