1.10. Add Your Own Methods to the String Class

Problem

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, so you can 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

Then invoke your 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 inside a class, object, or 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)
}

Note

See Recipe 6.7 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. 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. 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 and accepts 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.

Note

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.

Get Scala Cookbook now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.