In this post we’re going to take a closer look at Scala implicits and various use cases.
Basics
Let’s first look at the basics. There’re 3 main basic uses of implicits, as an argument, as a conversion method, and enhancing an existing class, a.k.a. the “Pimp My Library” pattern.
Implicit arguments
Suppose we have a basic function like this.
def plus(x: Int) = x + 1
plus(10) // => 11
We can add a second argument and make it a curried function.
def plus(x: Int)(y: Int) = x + y
plus(10)(1) // => 11
We can then make the second argument implicit and supply it via an implicit val
.
def plus(x: Int)(implicit y: Int) = x + y
implicit val one = 1
plus(10) // => 11
Since plus
needs an implicit argument of type Int
and there happens to be one in the scope, one
is applied automatically. However it won’t work if there are multiple implicit val
s.
implicit val one = 1
implicit val two = 2
plus(10) // => ambiguous implicit values
This example isn’t very interesting and one can usually use argument with a default value instead. However implicit arguments are handy for decoupling behavior from business logic. Let’s take a look at Future[+T]
for example.
val f = Future {
Thread.sleep(1000) // simulating slow computation
42
}
// => Cannot find an implicit ExecutionContext.
A closer look at the API shows that Future { code }
is actually an apply
method on the companion object. The implicit argument executor
determines the concurrency behavior, e.g. fixed, fork-join or other thread pools.
object Future {
def apply[T](body: => T)(implicit executor: ExecutionContext): Future[T]
}
And we can import a default global one in this case.
import scala.concurrent.ExecutionContext.Implicits.global
val f = Future { /* lenghty computation */ }
And global
is just a predefined implicit val
.
object ExecutionContext {
object Implicits {
implicit lazy val global: ExecutionContextExecutor = // ...
}
}
Implicit conversions
Say we build a complex number data type and a plus method.
case class Complex(r: Double, i: Double)
def plus(x: Complex, y: Complex): Complex =
Complex(x.r + y.r, x.i + y.i)
plus(Complex(1.0, -1.0), Complex(2.0, 0.0)) // Complex(3.0, -1.0)
Note that a complex number 2.0+0.0i
is also a real number, but we can’t pass it to plus
here.
plus(Complex(1.0, -1.0), 2.0) // type mismatch
We can however convert a Double
to a Complex
.
def double2complex(x: Double) = Complex(x, 0.0)
plus(Complex(1.0, -1.0), double2complex(2.0)) // Complex(3.0, -1.0)
And if we make double2complex
implicit the compiler can apply automatic conversion for us.
implicit def double2complex(x: Double) = Complex(x, 0.0)
plus(Complex(1.0, -1.0), 2.0) // Complex(3.0, -1.0)
Implicit conversions are handy when building specific types from broader, more generic ones, but could lead to surprises if abused. Imagine what happens if we provide an implicit conversion from String
to Int
.
def plus(x: Int, y: Int) = x + y
implicit def s2i(s: String) = s.length
plus("a", "bcd") // => 4!
Pimp My Library
Let’s say we’re using the Complex
type from another library and would like to add a scale
method to it. This is normally not possible without modifying the Complex
class source code. However we could build a wrapper for the class with additional methods.
class RichComplex(self: Complex) {
def scale(x: Double) = Complex(self.r * x, self.i * x)
}
We can also provide an implicit conversion method and use Complex
as if it’s also an RichComplex
. Here the compiler realizes that there’s no scale(x: Double)
method on Complex
but there exists an implicit conversion to RichComplex
which does.
implicit def makeRichComplex(self: Complex) = new RichComplex(self)
Complex(1.0, -1.0).scale(10.0) // => Complex(10.0, 10.0)
If we own the Complex
source code, a common pattern is to put enrichment methods (makeRich*
) in a companion object and they can be resolved automatically. This is a common technique for library builders to split up complex classes into modules and provide specialization.
trait MyList[T] {
// a lot of basic methods
}
class MyDoubleList(self: MyList[Double]) {
// specific methods for Double
}
class MyKeyValueList[K, V](self: MyList[(K, V)]) {
// specificmethods for key-value pairs
}
class MyTextList(self: MyList[String]) {
// specific methods for String
def saveAsTextFile(file: String) = // ...
}
object MyList {
implicit def makeDoubleList(self: MyList[Double]) =
new MyDoubleList(self)
implicit def makeKeyValueList[K, V](self: MyList[(K, V)]) =
new MyKeyValueList(self)
implicit def makeTextList(self: MyList[String]) =
new MyTextList(self)
}
You might have noticed that every time we call a method on an enhanced class, a new object is created. This can be sub-optimal, especially if there are large amount of base objects in a tight loop. The answer to this is value classes. Basically an implicit class that extends AnyVal
.
implicit class RichComplex(val self: Complex) extends AnyVal {
def scale(x: Double) = Complex(self.r * x, self.i * x)
}
The plus side is that now we don’t need the implicit def
conversion method any more. The slight down side is that implicit classes cannot be declared at top level. A common practice is to put these classes in a package object so the library user can import it easily, e.g. import com.mydomain.mydsl._
with the following example.
package com.mydomain
package object mydsl {
implicit class RichComplex(val self: Complex) extends AnyVal {
// ...
}
}
Type classes
Now that we covered the basic mechanics of implicits, let’s talk about it’s most common use case, type classes. We will start with summing numbers and then generalizing it to other types.
Summing numbers
Let’s start with a sum
method that sums Int
s.
def sum(xs: List[Int]) = xs.reduce(_ + _)
This is fairly straight-forward, but what if we want to sum Long
, Float
and Double
as well? The follow code actually doesn’t work because of type erasure.
def sum(xs: List[Int]) = xs.reduce(_ + _)
def sum(xs: List[Long]) = xs.reduce(_ + _)
def sum(xs: List[Float]) = xs.reduce(_ + _)
def sum(xs: List[Double]) = xs.reduce(_ + _)
But we noticed that all 4 cases require a _ + _
operation. We can define a trait Plus[T]
for it, which describes the plus behavior of type T
, and 4 instances of it, one for each numeric type we want to generalize over. We call Plus[T]
a type class for T
. Note that the instances are implicits because we want the compiler to pass them for us.
trait Plus[T] {
def plus(x: T, y: T): T
}
implicit val intPlus = new Plus[Int] {
override def plus(x: Int, y: Int): Int = x + y
}
implicit val longPlus = new Plus[Long] {
override def plus(x: Long, y: Long): Long = x + y
}
implicit val floatPlus = new Plus[Float] {
override def plus(x: Float, y: Float): Float = x + y
}
implicit val doublePlus = new Plus[Double] {
override def plus(x: Double, y: Double): Double = x + y
}
How we can rewrite our sum
method in a more generic way.
def sum[T](xs: List[T])(implicit p: Plus[T]) = xs.reduce(p.plus)
sum(List(1, 2, 3)) // => 6
sum(List(0.1, 0.2, 0.3)) // => 0.6
This is nice but we still have to implement 4 Plus[T]
instances. Lucky for us, there’s already a predefined Numeric[T]
type class in Scala, so we can simplify the above.
def sum[T](xs: List[T])(implicit num: Numeric[T]) = xs.reduce(num.plus)
This works for the numeric types in Scala but not necessarily other types that exhibit similar behavior, e.g. Complex
, Rational
. We can even further generalize the plus operation to non-numerical types, like the union of 2 sets.
Semigroup
Now we start noticing a common pattern among these use cases, a type T
and a binary operation plus
of (T, T) => T
. This is known as a semigroup in abstract algebra, and the plus operation is required to be associative, i.e. (x + y) + z = x + (y + z)
. We can now support Set[U]
by rewriting sum
with Semigroup[T]
.
trait Semigroup[T] {
def plus(x: T, y: T): T
}
implicit def numSg(implicit num: Numeric[T]) = new Semigroup[T] {
override def plus(x: T, y: T): T = num.plus(x, y)
}
implicit def setSg[U] = new Semigroup[Set[U]] {
override def plus(x: Set[U], y: Set[U]): Set[U] = x ++ y
}
def sum[T](xs: List[T])(implicit sg: Semigroup[T]) = xs.reduce(sg.plus)
sum(List(Set("a", "b"), Set("a", "c"), Set("d"))) // => Set("a", "b", "c", "d")
The method signature for sum
is a bit hard to read though, since we have a type parameter [T]
before the argument list (xs: List[T])
, and a implicit argument sg: Semigroup[T]
after that describes the behavior of T
. This can be rewritten as a context bound, [T: Semigroup]
, to indicate that there exists a Semigroup[T]
instance, but not explicitly given a name inside sum
. The inside the function implicitly[Semigroup[T]]
recovers the name.
def sum[T: Semigroup](xs: List[T]) = {
val sg = implicitly[Semigroup[T]]
xs.reduce(sg.plus)
}
And if we look at the source code of implicitly
, it simply does what we did with (implicit sg: Semigroup[T])
with a funny comment.
// for summoning implicit values from the nether world
def implicitly[T](implicit e: T) = e
One common practice in Scala is to use tuples for lightweight data representation, e.g. rows from a data source. But how do we sum tuples? Say we have a list of (Int, Double, Set[String])
, logically we want to sum the first, second and third field separately. Since we already have Semigroup[T]
on Int
, Double
and Set[U]
, we can compose them into a new semigroup. Note that this semigroup not only works for (Int, Double, Set[String])
but any tuple 3 with arbitrary member types.
implicit def t3Sg[A: Semigroup, B: Semigroup, C: Semigroup] = new Semigroup[(A, B, C)] {
val sgA = implicitly[Semigroup[A]]
val sgB = implicitly[Semigroup[B]]
val sgC = implicitly[Semigroup[C]]
(sgA.plus(x._1, y._1), sgB.plus(x._2, y._2), sgC.plus(x._3, y._3))
}
sum(List(
(1, 0.5, Set("a")),
(2, 1.5, Set("b", "c")),
(3, 2.5, Set("a", "b", "d"))))
// => (6, 4.5, Set("a", "b", "c", "d"))
Obviously we don’t want to handcraft this for tuples from 2 to 22. Libraries like Algebird already include these common ones.
We can even generalize semigroup to maps. For two maps of Map[K, V]
and a Semigroup[V]
, we can sum the maps by summing values of the same key with the given semigroup.
implicit def mSg[K, V: Semigroup] = new Semigroup[Map[K, V]] {
override def plus(x: Map[K, V], y: Map[K, V]): Map[K, V] =
x ++ y.map { case (k, rv) =>
val v = x.get(k) match {
case Some(lv) => implicitly[Semigroup[V]].plus(lv, rv)
case None => rv
}
(k, v)
}
}
val m1 = Map(
"a" -> (1, 0.5, Set("a")),
"b" -> (2, 1.5, Set("b", "c")),
"c" -> (3, 2.5, Set("a", "b", "c")))
val m2 = Map(
"a" -> (10, 10.0, Set("a", "b")),
"b" -> (20, 20.0, Set("c", "d", "e")),
"d" -> (40, 100.0, Set("z")))
sum(List(m1, m2))
/*
=> Map(
"a" -> (11, 10.5, Set("a", "b")),
"b" -> (22, 21.5, Set("b", "c", "d", "e")),
"c" -> (3, 2.5, Set("a", "b", "c")),
"d" -> (40, 100.0, Set("z")))
*/
Summary
That summarizes some main use cases of implicits. Here are some references.
- Ordering and Numeric type classes in Scala
- Algebird - Abstract Algebra for Scala
- My slides on type classes and semigroups
- Another excellent blog post on implicit design patterns by Li Haoyi
We didn’t discuss some more advanced topics like implicit resolution precedence and priority trick. Here are some more references.
Comments
comments powered by Disqus