1. Introduction
In this tutorial, we’ll find out what rich wrappers are and how we can use them in Scala.
The definition of “rich wrappers” lies in the name itself. First, they enrich some other class, that is, they add features the “original” class lacks.
Secondly, they wrap the other class, meaning that they should, as much as possible, behave as the class they wrap. Ideally, there should be no visible difference between using a wrapper or the original class.
Although we’ll briefly see how to create our own wrappers, we’ll focus more on those available in the language.
2. Shortcomings of Alternative Approaches
There are many ways to enrich something. For example, if we had access to the original class, we could add methods. Otherwise, we could use inheritance to create a subclass with the desired additional properties.
Nonetheless, while these two approaches achieve the “enrich” part, they fundamentally lack the “wrapper” part. For that, we could create a new, unrelated class and add our new features to it. For example, we could write our own RichInt with a countDigits method. Unfortunately, there would then be a big difference between using Int or RichInt, breaking the definition again.
Luckily, Scala has its own way of defining and using rich wrappers, and, as usual, it aims to reduce the boilerplate as much as possible.
Let’s dive into it in the next sections.
3. Rich Wrappers in Scala: a Helicopter View
Before looking at the rich wrappers defined by Scala, let’s see how we can create our own. Suppose we want to enrich Int with a way to count the digits of the number:
class SimpleRichInt(wrapped: Int) {
val digits: Int = wrapped.toString.length
}
SimpleRichInt is really simple. It just takes an Int and defines a member, digits, that counts the digits of the number by converting it to a String. Let’s see how we can use it:
assert(new SimpleRichInt(105).digits == 3)
While SimpleRichInt enriched Int, it is not a wrapper, as we have to instantiate it explicitly.
Let’s see how we can solve this:
object RichIntImplicits {
implicit class RichInt(wrapped: Int) {
val digits: Int = wrapped.toString.length
}
}
In the example above, we defined a new implicit class, RichInt, inside an object RichIntImplicits. The keyword implicit in the class definition makes the class’s primary constructor eligible for implicit conversions when the class is in scope.
That’s also why we defined it inside an object — so that we can easily import it when needed:
import RichIntImplicits._
assert(105.digits == 3)
By importing the RichIntImplicits._ object, we bring RichInt into the scope. Now, we can call digits on 105 without having to instantiate the new class explicitly. The compiler automatically takes care of the conversion from Int to RichInt.
However, we could’ve achieved the same result by defining the implicit conversion ourselves. Let’s see how we can do that on our SimpleRichInt:
object SimpleRichInt {
implicit final def SimpleRichInt(n: Int): SimpleRichInt = new SimpleRichInt(n)
}
And then we can use it as a real rich wrapper:
import SimpleRichInt._
assert(105.digits == 3)
4. Rich Wrappers in Scala’s Standard Library
In this section, we’ll explore how Scala implements its rich wrappers. In particular, we’ll look at common types such as Char, Int, and Boolean, along with String and Array types.
4.1. Common Types
Scala’s Predef follows the second approach presented in the previous section. It contains several implicit conversions allowing programmers to create rich wrappers easily.
Let’s see some examples for the most common types:
@inline implicit def byteWrapper(x: Byte) = new runtime.RichByte(x)
@inline implicit def shortWrapper(x: Short) = new runtime.RichShort(x)
@inline implicit def intWrapper(x: Int) = new runtime.RichInt(x)
@inline implicit def charWrapper(c: Char) = new runtime.RichChar(c)
@inline implicit def longWrapper(x: Long) = new runtime.RichLong(x)
@inline implicit def floatWrapper(x: Float) = new runtime.RichFloat(x)
@inline implicit def doubleWrapper(x: Double) = new runtime.RichDouble(x)
@inline implicit def booleanWrapper(x: Boolean) = new runtime.RichBoolean(x)
Those functions are defined in a superclass of Predef, LowPriorityImplicits.
Since Scala 2.8, the implicit resolution system is priority-based. When comparing two different suitable alternatives for an implicit method, each one gets one point for having more specific arguments or for being defined in a subclass. The compiler picks the alternative that gets more points. Hence, since Predef extends LowPriorityImplicits, other implicit conversions defined in Predef take precedence over those defined in LowPriorityImplicits.
All the conversions defined in Predef are accessible without any further import. If our rich wrappers conflict with Scala’s built-in rich wrappers, the compiler will pick our version only if we import the right implicit conversion. For example, in the previous section, we defined our own version of RichInt and we had to import it explicitly to let the compiler know which one we wanted.
If we hadn’t done that, the compiler would’ve tried to apply Predef‘s implicit conversion. Nonetheless, Scala’s RichInt does not define digits, so the compiler would have failed with an error.
Let’s put this theory into practice and see how we can easily format a number into its binary and hexadecimal representations:
assert(3.toBinaryString == "11")
assert(10.toHexString == "a")
4.2. String
The next rich wrapper we’ll look at is StringOps. The implicit conversions defined in Predef are similar to the previous ones:
@inline implicit def augmentString(x: String): StringOps = new StringOps(x)
@inline implicit def unaugmentString(x: StringOps): String = x.repr
Let’s see some useful methods provided for free by this wrapper:
assert("test".reverse == "tset")
assert("test"(1) == 'e')
In the first assertion, we obtain the reverse of the original string simply by calling reverse. On the other hand, the second example shows how StringOps defines an apply method that returns a character at a given position in the string.
4.3. Array
Finally, arrays have their own rich wrapper as well:
implicit def genericArrayOps[T](xs: Array[T]): ArrayOps[T] = (xs match {
case x: Array[AnyRef] => refArrayOps[AnyRef](x)
case x: Array[Boolean] => booleanArrayOps(x)
case x: Array[Byte] => byteArrayOps(x)
case x: Array[Char] => charArrayOps(x)
case x: Array[Double] => doubleArrayOps(x)
case x: Array[Float] => floatArrayOps(x)
case x: Array[Int] => intArrayOps(x)
case x: Array[Long] => longArrayOps(x)
case x: Array[Short] => shortArrayOps(x)
case x: Array[Unit] => unitArrayOps(x)
case null => null
}).asInstanceOf[ArrayOps[T]]
implicit def booleanArrayOps(xs: Array[Boolean]): ArrayOps.ofBoolean = new ArrayOps.ofBoolean(xs)
implicit def byteArrayOps(xs: Array[Byte]): ArrayOps.ofByte = new ArrayOps.ofByte(xs)
implicit def charArrayOps(xs: Array[Char]): ArrayOps.ofChar = new ArrayOps.ofChar(xs)
implicit def doubleArrayOps(xs: Array[Double]): ArrayOps.ofDouble = new ArrayOps.ofDouble(xs)
implicit def floatArrayOps(xs: Array[Float]): ArrayOps.ofFloat = new ArrayOps.ofFloat(xs)
implicit def intArrayOps(xs: Array[Int]): ArrayOps.ofInt = new ArrayOps.ofInt(xs)
implicit def longArrayOps(xs: Array[Long]): ArrayOps.ofLong = new ArrayOps.ofLong(xs)
implicit def refArrayOps[T <: AnyRef](xs: Array[T]): ArrayOps.ofRef[T] = new ArrayOps.ofRef[T](xs)
implicit def shortArrayOps(xs: Array[Short]): ArrayOps.ofShort = new ArrayOps.ofShort(xs)
implicit def unitArrayOps(xs: Array[Unit]): ArrayOps.ofUnit = new ArrayOps.ofUnit(xs)
Scala defines two different implicit conversions here. First, genericArrayOps takes care of generic arrays (Array[T]). In this case, Scala resolves the actual type T via pattern matching.
Then, based on the type, another conversion function, such as booleanArrayOps, if T=Boolean, is called. However, booleanArrayOps is also an implicit function. This is because the type of the elements in an array may be known without the need for pattern matching.
*For example, if a value has type Array[Boolean] (not Array[T]), the function booleanArrayOps is more specific and is used by the compiler instead of its generic counterpart.*
Let’s see how we can use ArrayOps to add elements to an array or to slice one:
assert(Array(1, 2, 3).:+(4).:+(5).sameElements(Array(1, 2, 3, 4, 5)))
assert(Array(1, 2, 3).slice(1, 2).sameElements(Array(2)))
The first example also shows how to concatenate several implicit conversions. Array(1, 2, 3).:+(4) returns an Array, which we convert to ArrayOps again to invoke the :+ method a second time.
5. Conclusion
In this article, we learned how rich wrappers are defined in Scala and how we can use them in our programs. In addition, we also briefly looked at how to define our own rich wrappers to add features to existing classes in a natural way.
As usual, the full source code can be found over on GitHub.