Home Type Classes
Post
Cancel

Type Classes

Type classes have been a blocker between me and getting a further understanding of the beauty of Functional Programming in Scala. Materials covering the matter tend to be mixed with a complex business problem or more likely some parser or JSON converter. I tried to strip all that away and create a step by step tutorial on how type classes work. I start with a TL;DR for both the inpatient and my future self. I would encourage you to go through the following sections where I go over the motivation for having type classes and how we tidy our way into a cleaner implementation. I left the theory and housekeeping until the end. You can go through the resources for more complex examples on the topic.

TL;DR

We want to use type classes to implement a serializer Writer that takes a Writable and use it to write a tuple of type (Int, Int)

  1. Create Your Writable
1
2
3
trait Writable[A] {
  def write(value: A): String
}
  1. Create your Writer
1
2
3
object Writer {
  def write[A: Writable](value: A): String = implicitly[Writable[A]].write(value)
}
  1. Create your implicit
1
2
3
implicit val writableTuple: Writable[(Int, Int)] = new Writable[(Int, Int)] {
  override def write(value: (Int, Int)) = s"${value._1},${value._2}"
}
  1. Use It
1
Writer.write((1, 2)) // res: String = 1,2

Why do we need type classes?

Let’s start with a trivial example.

1
2
3
4
trait Item
case class Item1(value: String) extends Item
case class Item2(value: Int) extends Item
case class Item3(value: Boolean) extends Item

Assume that we have the following “serializer”. It takes a writable and returns its string value.

1
2
3
4
5
6
7
object Writer {
  def write(writable: Writable): String = writable.write
}

trait Writable{
  def write:String
}

What we want to do, is to be able to write our Item instances using the Writer. The quickest approach would be to make Item extend Writable.

1
2
3
4
5
6
7
8
9
10
11
12
13
trait Item extends Writable

case class Item1(value: String) extends Item{
  override def write = value
}

case class Item2(value: Int) extends Item{
  override def write = value.toString
}

case class Item3(value: Boolean) extends Item{
  override def write = value.toString
}

Now we can use Writer.write(Item1("Hello World!")) to get our string. This trivial example while works, it suffers from some drawbacks, including:

  • Implementing the Writable trait is not possible if Item comes from a library we don’t control.
  • It couples Item with Writer.
  • We polluted our case classes. write is not a concern for an Item to have.
  • For a more complex data structure, there is likely to be a bit more copying an pasting and redundant code.

The Solution

Step 1: Decoupling

Let’s redesign our Writer by making the method write polymorphic taking a value of type A and a Writable for that type.

1
2
3
4
5
6
7
object Writer {
  def write[A](value: A, writable: Writable[A]): String = writable.write(value)
}

trait Writable[A]{
  def write(value: A):String
}

Now we get to restore our clean Items and externalize the code to write them. Let’s focus on Item1 for example.

1
2
3
4
5
6
7
case class Item1(value: String) extends Item

val Item1Writable = new Writable[Item1] {
  override def write(value: Item1): String = value.value
}

Writer.write(Item1("Hello World!"), Item1Writable)

Nothing fancy, nor Scala specific here. Any language that has some notion of generics can implement this solution.

We are still, however, a little bit more verbose and we have many pieces moving around.

Step 2: Tidy Up Via Type Classes

Scala implicits to the rescue. In this step, we will change the Writer.write method by making the Writable parameter implicit. Then declare Item1Writable as an implicit.

1
2
3
4
5
6
7
8
9
10
object Writer {
  def write[A](value: A)(implicit writable: Writable[A]): String
}

implicit val Item1Writable = new Writable[Item1] {
  override def write(value: Item1): String = value.value
}

Writer.write(Item1("Hello World!"))

Voila, now we can write Writer.write(Item1("Hello World!")) the same way it was originally without needing to pass in the writable explicitly.

Step 3: Better Syntax via Context Bound

Context bound gives us a way to be a little bit less verbose, by changing A to be context bound by Writable i.e. A:Writable (which means that A has an associated Writable[A]) we can get rid of the implicit parameter and use the keyword implicitly to pull a Writable out of the implicit scope. We don’t need to make changes to anything else, and we still have the same result.

1
2
3
4
def write[A:Writable](value: A): String = {
    val writable = implicitly[Writable[A]]
    writable.write(value)
  }

Which we can rewrite by inlining the writable val:

1
def write[A:Writable](value: A): String = implicitly[Writable[A]].write(value)

One other gift from Scala, when defining our implicits, we can use a lambda instead of a trait with a single method. So we can rewrite

1
2
3
implicit val Item1Writable: Writable[Item1] = new Writable[Item1] {
  override def write(value: Item1): String = value.value
}

as

1
implicit val Item1Writable: Writable[Item1] = value => value.value

just a little bit less text to deal with. Of course, if the type class has more than one method, we are out of luck.

Put It All Together

By convention, we put our type classes implicits in the companion object that we control. So, if we own both Writable and Item, we can put them in either. I will go with Writable to keep them in the same place. If we own only one, our choices are limited. It is convenient to put the implicits for primary data types such as String, Int, etc. in the companion object of the type class.

Now, to the final reveal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
trait Item
case class Item1(value: String) extends Item
case class Item2(value: Int) extends Item
case class Item3(value: Boolean) extends Item

object Writer {
  def write[A: Writable](value: A): String = implicitly[Writable[A]].write(value)
}

trait Writable[A] {
  def write(value: A): String
}

object Writable {
  implicit val Item1: Writable[Item1] = value => value.value
  implicit val Item2: Writable[Item2] = value => value.value.toString
  implicit val Item3: Writable[Item3] = value => value.value.toString
}

Writer.write(Item1("Hello World!"))
Writer.write(Item2(2))
Writer.write(Item3(true))

You can also live edit this code gist on Scastie

Reap The Rewards

Let’s say we have a new class that we want to write. Now, it is easy to do that by adding few implicits.

1
2
3
4
5
6
7
case class Person(name:String, age: Int)

object Person{
  implicit val writable: Writable[Person] = value => s"I am ${value.name} and I am ${value.age} years old."
}

Writer.write(Person("Jack", 18))

If we don’t own the Writer code, and we don’t own the type we want to write all we need is an implicit in scope. Here is an example for a tuple.

1
2
3
implicit val writable: Writable[(String, String)] = value => s"Left: '${value._1}'. Right: '${value._2}'"

Writer.write(("Apple", "Banana"))

The Theory

The Reasoning behind type classes

  • Monomorphic functions know too much
  • Polymorphic functions throw away too much.
  • Type classes provide a way to regain structure but as little as possible (or only as much s needed)
  • Passing in a function is an ad-hock alternative
  • Type classes to the rescue as a form of ad-hoc polymorphism
  • Scala doesn’t have native support of type classes. We fake type classes in Scala using traits or abstract classes

Type Classes should include:

  1. Types
  2. Operations on values of types
  3. Laws governing the behaviour of those operations. Conveyed in name and signature, not in comments, if it is all possible.

    Housekeeping and Final Thoughts

If you don’t like the unfriendly message that Scala spits when it can’t find an implicit in scope, you can customize it using implicitNotFound annotation over the type class.

1
2
3
4
5
import annotation.implicitNotFound
@implicitNotFound("Oops! couldn't find an implicit for Writable in scope for ${T}")
trait Writable[A] {
  def write(value: A): String
}

Avoid ambiguous implicits. Don’t create multiple implicits for the same type and same type class. It is inconvenient, if not dangerous that the meaning of our code would change depending on which implicit is in scope. To combat this use “new types” , which is a Haskell notation. Create a new type by wrapping your type by a shallow case class and create implicits for the new one. You can consult any of the resources below for more complex examples where type classes shine.


Resources

  1. Functional Scala by John A. De Goes (Toronto Edition)
  2. Tutorial: Typeclasses in Scala with Dan Rosen
  3. The Neophyte’s Guide to Scala Part 12: Type Classes
  4. stackoverflow: What is a “context bound” in Scala?
  5. Deferring commitments: Tagless Final
This post is licensed under CC BY 4.0 by the author.
Contents

SemiGroup

How to setup github pages with an https custom domain?

Comments powered by Disqus.