We recently had an internal knowledge sharing on higher-kinded types and CanBuildFrom type classes in Scala. Here’s a short summary.

Basics

Let’s start by implementing map.

def map(xs: Seq[Int], f: Int => Double): Seq[Double] = xs.map(f)
map(Seq(1, 2, 3), _ + 0.1)

This implementation is not very good since it only works with Seq[Int] and Int => Double. It’s easy to parameterize Int and Double.

def map[A, B](xs: Seq[A], f: A => B): Seq[B] = xs.map(f)

However map(Seq(1, 2, 3), _ + 0.1) now fails to compile with a message missing parameter type for expanded function ((x$1) => x$1.$plus(10))

This is because inference of A in f: A => B depends on the type of xs: Seq[A], and limitation of Scala type inference. A common workaround is to curry arguments.

def map[A, B](xs: Seq[A])(f: A => B): Seq[B] = xs.map(f)
map(Seq(1, 2, 3))(_ + 0.1)

Similar pattern is commonly seen in Scala, like foldLeft(z: B)(op: (B, A) => B). Another benefit is we can now write f in a multi-line {} block more elegantly.

map(Seq(1, 2, 3)) { x =>
  val y = x + 0.1
  println(s"$x + 0.5 = $y")
  y
}

Higher-kinded types

However this version still only works on Seq[A]. We could parameterize it as M[_], also known as higher-kinded type.

import scala.language.higherKinds
def map[M[_], A, B](xs: M[A])(f: A => B): M[B] = xs.map(f)

But now we have a compiler error saying value map is not a member of type parameter M[A]. We can add a upper bound for M[_] but it still wouldn’t work.

def map[M[_] <: Seq[A], A, B](xs: M[A])(f: A => B): M[B] = xs.map(f)

Despite the fact that M[_] is a sub-type of Seq[A], and Seq[A] has a map method, we can’t build a M[B] back from Seq[A]#map[B](f: A => B), since M[B] is a more specific type than Seq[B].

type mismatch;
[error]  found   : Seq[B]
[error]  required: M[B]

However if we look at the method signature of Seq[A]#map(f: A => B) it actually looks like this.

trait TraversableLike[+A, +Repr] {
  def map[B, That](f: A => B)(implicit bf: CanBuildFrom[Repr, B, That]): That
}

Note that it’s a method on trait TraversableLike, which means any TraversableLike collection can share the same map implementation. Also worth noting is Seq[+A] has an inheritance hierarchy like this:

trait Seq[+A] extends SeqLike[A, Seq[A]] // ...
trait SeqLike[+A, +Repr] extends IterableLike[A, Repr] // ...
trait IterableLike[+A, +Repr] extends TraversableLike[A, Repr] // ...

We see that in Seq[A] extends SeqLike[A, Seq[A]], type of the current trait Seq[A] is also used as a type parameter for the super-type, which is propagated all the way to TraversableLike. This is commonly known as F-bounded polymorphism, which is useful heere since methods on sub-types of TraversableLike can now return a more specific type, e.g. Seq, List, Vector, instead of the same broad super-type. For more information on F-bounded polymorphism, check out Marconi Lanna’s excellent presentation at NEScala 2015.

In the case of Seq[Int]#map(f: Int => Double), we can expand the method like this:

def map[Int, That](f: Int => Double)(implicit bf: CanBuildFrom[Seq[Int], Double, That])

CanBuildFrom

CanBuildFrom is a key type class in the Scala collections library and enables a lot code reuse.

trait CanBuildFrom[-From, -Elem, +To]

In its type signature, From is the original collection and To is the target collection. Elem is the element type of the target collection and in some determines the type of To. For example we can map over a Map[K, V] into K -> V (which is a syntactic sugar for (K, V)), and get another map. However mapping into String would result in a List[String] since one cannot build a Map[K, V] with a single type.

// From: Map[String, Int]
// (String, Int) => (String, Double)
// To: Map[String, Double]
// CanBuildFrom[Map[String, Int], (String, Double), Map[String, Double]]
Map("a" -> 1, "b" -> 2).map(kv => kv._1 -> kv._2 + 0.1)

// From: Map[String, Int]
// (String, Int) => String
// To: List[String]
// CanBuildFrom[Map[String, Int], String, List[String]]
Map("a" -> 1, "b" -> 2).map(kv => kv._1 + kv._2.toString)

We can use CanBuildFrom to rewrite our map function.

def map[M[_] <: Seq[A], A, B](xs: M[A])(f: A => B)
                             (implicit cbf: CanBuildFrom[M[A], B, M[B]]): M[B] = {
  val b = cbf() // new Builder[Elem, To]
  xs.foreach(x => b += f(x))
  b.result()
}

Our function can now support any Seq types without losing the type information.



Comments

comments powered by Disqus