Infix functions with Scala type classes

January 05, 2014

Out of the box, Scala supports infix functions via a syntactic nuance; a function with a symbolic name ending in a colon may be written to the left of the instance on which it is defined.

Consider the class MyNum:

case class MyNum(x: Int) {
  def +:(y: Int): Int = x + y
}

Because the addition function +: ends in a colon, we can use it as both a prefix and an infix function:

val x = MyNum(21).+:(21) // 42
val y = 21 +: MyNum(21)  // 42

This is handy, but somewhat syntactically limited. Fortunately, we can do better.

Consider the trait Semigroup:

trait Semigroup[A] {
  def append(a1: A, a2: A): A
}

Any type A can have a semigroup if we can implement append for it:

val listSemigroup =
  new Semigroup[List[Int]] {
    def append(a1: List[Int], a2: List[Int]): List[Int] = a1 ++ a2
  }
import listSemigroup._

val list = append(List(1,2,3), List(4,5,6)) // List(1,2,3,4,5,6)

So far, we have a prefix-notation function append that takes two lists, a1 and a2, and concatenates them. Let's augment Semigroup, adding a type class that lifts a1 into a new type that has its own append function:

trait Semigroup[A] {
  def append(a1: A, a2: A): A
  class InfixSemigroup(a1: A) {
    def ⋅(a2: A): A = Semigroup.this.append(a1, a2)
  }
  implicit def infix(a1: A) = new InfixSemigroup(a1)
}

We can use our same listSemigroup implementation, which now has an implicit infix function to convert a1 into an InfixSemigroup instance, which has its own append function called :

val listSemigroup =
  new Semigroup[List[Int]] {
    def append(a1: List[Int], a2: List[Int]): List[Int] = a1 ++ a2
  }
import listSemigroup._

val list = List(1,2,3) ⋅ List(4,5,6) // List(1,2,3,4,5,6)