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"
)
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.
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 String
Utils
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
)
}
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.
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
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:
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 and accepts 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.
Note
For more details on what’s happening here, see SIP-13, Implicit Classes.
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
(
"."
,
"*"
)
}
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.