Scala context bound

on waitingforcode.com

Scala context bound

This post begins the series of posts about Scala's features called "One Scala feature per week". Every week one particular Scala's functionality will be covered. This beginning post will explain context bound.

Scala has a rich types ecosystem. They allow to do much more than Java's classical generics and wildcards. But since the context bounds are related to other interesting concepts, this post will begin with only a short and general context bounds definition. In the second part it'll describe ad-hoc polymorphism strongly related to them. And in the last part it'll give an example of the context bounds in the type class pattern.

Context bound definition

After some digging I found a lot of different definitions for the context bound. Since it's difficult to chose one universal and clear definition, some of the most meaningful ones are listed below (references are listed at the end of the post):

  • context bound is a has-a constraint between a type parameter and a type class[1]
  • context bound describes implicit value instead of view bound's implicit conversion[2]
  • context bound is a way of asserting the existence of an implicit value[3]

All these descriptions have one common point - implicit value. To summarize them we can tell that the context bound declares that for some type its implicit value exists somewhere in the compilation scope. The context bound is written as T : M. It requires the existence of an implicit value for M[T]. For now it's enough. We'll deepen this definition in the next 2 sections.

Ad-hoc polymorphism

Another concept that should help to understand the context bounds is ad-hoc polymorphism. This category of polymorphism bounds to the type. It means that a different implementations for given action will be called for every type, i.e.: the method A for the String type, the method B for Int type and so on. A dummy example of an ad-hoc polymorphism can be the factory method (only for illustration, it doesn't compile):

object FactoryAdder {
  def apply[T](value1: T, value2: T): Option[T] = {
    (value1, value2) match {
      case (text1: String, text2: String) => {Some(s"$text1$text2")}
      case (number1: Int, number2: Int) => {Some(number1 + number1_2)}
      case _ => None
    }
  }
}

The ad-hoc polymorphism is the contrary of the parametric polymorphism that is based on function parameters rather than on their types. An example of this kind of polymorphism is the sequence's method head() returning the first element of the collection. It has a single one implementation that doesn't depend on the collection's type.

Type class pattern

The ad-hoc polymorphism brings another concept helpful in understanding the context bounds - type class pattern. This pattern is a construct supporting ad-hoc polymorphism. In Scala it can be constructed with the help of context bounds. It's composed of:

  • traits describing some behavior (called type class)
  • type parameters
  • type class instances - specific implementations for the behavior described in the type class
  • polymorphic operations receiving other instances of type classes via Scala's implicit parameters

For our use case of type class pattern we want to expose a method combining the values of the same type passed in the parameter. Something like:

def combine(items: Seq[A]): A

Regarding to previously listed points in order to define type pattern in Scala we should use:

  • traits describing some behavior:
      trait Combinable[A] {
        def combine(items: Seq[A]): A
      }
      
  • type parameters: A
  • type class instances:
      object Combinable {
        implicit val combinableInt = new Combinable[Int] {
          override def combine(items: Seq[Int]): Int = items.sum
        }
        implicit val combinableString = new Combinable[String] {
          override def combine(items: Seq[String]): String = items.mkString(", ")
        }
      }
      
  • polymorphic operations receiving other instances of type classes via Scala's implicit parameters:
        def combineItems[A](a: Seq[A])(implicit ev: Combinable[A]) =  ev.combine(a)
      

So constructed type class pattern will perfectly work for the tests defined below:

class ContextBoundSpec extends FunSpec with Matchers {

  describe("combinable type class pattern") {
    it("should work for string values") {
      val result = ContextBoundExposer.combineItems(Seq("a", "b", "c"))

      result shouldEqual "a, b, c"
    }
    it("should work for int values") {
      val result = ContextBoundExposer.combineItems(Seq(1, 2, 3))

      result shouldEqual 6
    }
    /*
      * Below code won't even compile because the assertion of the implicit type existence fails - the
      * implicit Double type is not defined:
      * it("should fail for not defined types") {
      *  Method.combineItems(Seq(3.0, 5.0))
      * }
      * The error should look like:
      * "Error:(279, 26) could not find implicit value for parameter ev: com.waitingforcode.types.Combinable[Double]
      * Method.combineItems(Seq(3.0, 5.0))"
      */
  }

}

To make simple, we can define type classes as a parametrized trait having separated implicit implementations for each of supported types in a companion object. They bring an advantage when we work with all kinds of 3rd part libraries. With the type classes we can easily add new implementations or override already existent ones without modifying the source code. Unfortunately it's widely based on the implicits that always bring some additional level of complexity in the debugging and code discovery.

But keep in mind that context bounds are not reserved to the type classes. The latter ones are only a good and clear example. The context bound simply requires the implicit availability of one of implementations for the typed trait at the point of invocation.

Share, like or comment this post on Twitter:

Share on: