1. Overview

In this tutorial, we’ll learn how to write generic type-safe code in Scala. Moreover, our library will be binary compatible with types unknown when we wrote and compiled our library.

Therefore, the users would be able to call our library methods with their types, without waiting for us to recompile our library to extend support for them. Also, we will write generic type-safe code without using inheritance or type casts.

This generic coding style is often referred to as Ad-Hoc Polymorphism, and Scala has first-class support for it. The Scala community has widely adopted a particular pattern of Ad-Hoc Polymorphism known as Type Classes, which is the basis for many Functional Programming Libraries.

2. A Review of Relevant Concepts

View and Context bounds are syntax sugar, and the compiler translates them using the more fundamental language constructs in the early phases of the compilation. In this section, we will review those features.

In Scala, we can define methods and constructors with multiple parameter lists. These are the basis for functional programming features like Currying and Partial Application.

We can also use them to write procedures that look like language control structures when used. In the following example, we create a method with two parameter lists, and the second takes a single parameter, which allows us to call it using curly braces instead of parenthesis:

object multipleparameterlists {
  val amIcool: Boolean = true

  def cond(pred: => Boolean)(proc: => Unit): Unit = {
    if (pred) proc else ()
  }

  cond(amIcool) {
    println("You are cool")
  }
}

Additionally, we can mark one of the parameter groups as implicit.

This instructs the compiler to complete the method call if values for that parameter list are not explicitly written in the call. It will search for variables marked as implicit and of the same type within the calling scope:

object implicitvalues {
  def printDebugMsg(msg: String)(implicit debugging: Boolean): Unit = {
    if (debugging) println(msg) else ()
  }

  implicit val debugging: Boolean = true

  printDebugMsg("I am debugging this method")
}

In the last line, we called the method with a single parameter list. Thus, the compiler completes the call by injecting the missing arguments using implicit variables within the scope. In this case, the implicit variable is the debugging variable.

2.1. Using Implicit Parameters to Implement Polymorphic Behaviours

Writing generic polymorphic code in a statically typed language can be difficult. However, we should strive to do it without resorting to escape hatches like Runtime Reflection or Type Casting.

We can do this by adding requirements to use our code; for instance, we can require the calling code to provide a function of a given type:

object conversion {
  abstract class Order[T](val me: T) {
    def less(other: T): Boolean
  }

  val intToOrder: Int => Order[Int] = x => new Order[Int](x) {
    override def less(other: Int): Boolean = me < other
  }

  def maximum[A](a: A, b: A)(toOrder: A => Order[A]): A = {
    if (toOrder(a).less(b)) b else a
  }

  val a = 5
  val b = 9

  println(s"The maximum($a, $b) is ${maximum(a, b)(intToOrder)}")
}

Unfortunately, as a result of adding the constraints, using our code becomes awkwardly verbose, because users have to pass the function explicitly on every call to our method.

We can eliminate the verbosity at the call site, by marking the constraining parameter list as implicit:

object implicitconversion {

  abstract class Order[T](val me: T) {
    def less(other: T): Boolean
  }

  implicit val intToOrder: Int => Order[Int] = x => new Order[Int](x) {
    override def less(other: Int): Boolean = me < other
  }

  def maximum[A](a: A, b: A)(implicit toOrder: A => Order[A]): A = {
    if (toOrder(a).less(b)) b else a
  }

  val a = 5
  val b = 9

  println(s"The maximum($a, $b) is ${maximum(a, b)}")
}

We can see here how our function maximum becomes easier to use, as we can now call it without explicitly passing the constraint function.

Type constraints give contextual information to the compiler about the operations that the types accepted by our library should support. This broadens the kind of application we can write without resorting to runtime tricks like reflection or type casts.

Scala has syntax support for two styles of constraints, requiring an implicit type conversion or a module of a specific compound type.

3. View Bounds

First, let’s discuss using implicit conversions as type constraints in our methods. The constraint is a type conversion, and we can think of it as Type A should be viewable as B. It also limits the types that will be accepted, therefore the View Bound name.

Using the “<%” type operator, we can simplify the code of the previous example:

object viewbound {

  abstract class Order[T](val me: T) {
    def less(other: T): Boolean
  }

  implicit val intToOrder: Int => Order[Int] = x => new Order[Int](x) {
    override def less(other: Int): Boolean = me < other
  }

  def maximum[A <% Order[A]](a: A, b: A): A = {
    val toOrder = implicitly[A => Order[A]]
    if (toOrder(a).less(b)) b else a
  }

  val a = 5
  val b = 9

  println(s"The maximum($a, $b) is ${maximum(a, b)}")
}

3.1. A Word of Caution About View Bounds

Implicit conversions are a controversial feature, and their use is now strongly discouraged.

First, Scala hid implicit conversions behind a language feature. To use it, we need to activate it explicitly by adding a compiler flag or a feature import. Most importantly, the language maintainers will deprecate it all together in the future.

4. Context Bounds

Haskell has influenced the Scala Language, and it’s from there that the idea of Type Classes came. Instead of requiring a Type Conversion, we will need a module implementing a set of functions.

Scala groups functions in objects at runtime. Therefore we can require the existence of a given context object, instead of declaring each constraining function by itself:

object implicitobject {

  abstract class Order[T] {
    def less(me: T, other: T): Boolean
  }

  implicit val intOrder: Order[Int] = new Order[Int] {
    override def less(me: Int, other: Int): Boolean = me < other
  }

  def maximum[A](a: A, b: A)(implicit ord: Order[A]): A = {
    if (ord.less(a, b)) b else a
  }

  val a = 5
  val b = 9

  println(s"The maximum($a, $b) is ${maximum(a, b)}")
}

As with context bounds, the language has syntax sugar to reduce the declaration and use verbosity:

def maximum[A : Order](a: A, b: A)(implicit ord: Order[A]): A

4.1. Using the Context Object

The Context bound syntax is nice and concise to use, but the evidence isn’t named, and we can only forward it to other methods. To use it directly, we can either choose to use the more verbose syntax or use the context-bound feature and then use implicitly to obtain a reference to the evidence in our code:

object contextbound {

  abstract class Order[T] {
    def less(me: T, other: T): Boolean
  }

  implicit val intOrder: Order[Int] = new Order[Int] {
    override def less(me: Int, other: Int): Boolean = me < other
  }

  def maximum[A: Order](a: A, b: A): A = {
    val ord = implicitly[Order[A]]
    if (ord.less(a, b)) b else a
  }

  val a = 5
  val b = 9

  println(s"The maximum($a, $b) is ${maximum(a, b)}")
}

In this example, we used the implicitly keyword to reference the context object and gave it a name. This allows us to use it and forward it to other methods.

4.2. Are Context Bounds Equally Powerful to View Bounds?

Most View Bound uses would use the conversion to implement a Rich Object pattern; We do this by passing the Rich Interface implicit class within the Context Bound.

Unfortunately, we lose some features, like calling extension methods directly, but we can get that back that by including an Implicit Class in the context object:

object richobject {

  abstract class Order[T] {
    def less(me: T, other: T): Boolean

    implicit class RichInterface(val me: T) {
      def <(other: T): Boolean = less(me, other)
    }

  }

  implicit val intOrder: Order[Int] = new Order[Int] {
    override def less(me: Int, other: Int): Boolean = me < other
  }

  def maximum[A: Order](a: A, b: A): A = {
    val ord = implicitly[Order[A]]
    import ord._
    if (a < b) b else a
  }

  val a = 5
  val b = 9

  println(s"The maximum($a, $b) is ${maximum(a, b)}")
}

By including an implicit class in the context object, the users of our methods have the option to import it in the relevant scope as we did in the example above.

This example demonstrates that context and view bounds are equivalent in power, and shows that the deprecation of view bounds doesn’t represent a loss of expressiveness for the Scala language.

5. Conclusion

In this tutorial, we explored how to use implicit parameters to write flexible generic code with two distinctive patterns for them: View and Context Bounds.

We also understood why we should avoid view bounds, but most importantly, how we can write equivalent code using the safer Context Bound approach.

As always, the full source code of the article is available over on GitHub.
guest
0 Comments
Inline Feedbacks
View all comments