Parametric polymorphism for functions in Scala

Posted on September 18, 2016

Let’s consider a simple typeclass:

trait Addable[T] {
  def plus(x: T, y: T): T
}

implicit object IntAddable extends Addable[Int] {
  def plus(x: Int, y: Int) = x + y
}

implicit object ListAddable extends Addable[List[Int]] {
  def plus(x: List[Int], y: List[Int]) = x ++ y
}

def plus[T](x: T, y: T)(implicit ev: Addable[T]) = ev.plus(x, y)

List(1, 2, 3).foldLeft(0)(plus) //> Int = 6
List(List(1), List(2), List(3)).foldLeft(List[Int]())(plus) //> List[Int] = List(1, 2, 3)

What happens here is called eta-expansion, and what foldLeft is getting is this function (e.g. for the former case):

new Function2[Int, Int, Int] {
  def apply(x: Int, y: Int): Int = plus[Int](x, y)
}

What’s wrong here? We wrap plus invocation in anonymous function and effectively transfer control twice with required stack reshuffle, and it happens for every element in a collection (details here). Can we do better?

Let’s change just one line:

def plus[T](implicit ev: Addable[T]) = (x: T, y: T) => ev.plus(x, y)

It still works, only now we instantiate Function2 by calling method plus once, and this function is passed to foldLeft. What is lost is the ability to call plus as a method (i.e. plus(1,2)).