Type classes for the Java Engineer

One of Scala’s strengths is its support for multiple programming paradigms. In practice, however, Scala reminds the developer how each paradigm evolved separately, and carried with it lexicons and perspectives for concepts that turn out to have a lot in common.

Type Classes were a concept I had difficulty appreciating given my Java background, until I realized that I could draw analogies with the Adapter and Strategy design patterns. At a high level, a Type Class is a set of operations, and a way of mapping types to those operations, external from the definition of those types.

The Adapter comes into play in the sense that we’re extending the API of a type so that it can work with another set of operations. Strategy can be recognized in the way that that mappings can be interchangable for a given type; the class diagram more closely resembles Strategy rather than Adapter.

An example: Comparators

The Strategy pattern allows for multiple implementations of an algorithm to exist, defined external to the objects it operates on, while the Adapter pattern allows for an existing API to be used in a new context without being directly modified.

An example of a Strategy most developers would be aware of is the Comparator<T> interface; each implementation can provide for alternate means by which the instance of type T can be compared. Here’s an example in Java:

1
2
3
4
5
6
7
8
9
10
11
12
public interface Comparator<T> {
    int compare(T lhs, T rhs);
}

// as found in package java.util
public class Collections {
    public static <T> List<T> sort(List<T> collection, Comparator<T> comaprator) { 
        ...
        if (comparator.compare(collection.get(i), collection.get(j) < 0) { ... }
        ...
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Foo { ... }

class Comparators {
  public static final Comparator<Foo> fooComparator = new Comparator<Foo>() {
    @Override
    public int compare(Foo lhs, Foo rhs) {
        ...
    }
  };
}

class Example {
    public static void main(String[] args) {
        ...
        List<Foo> foos = ...            
        List<Foo> sorted = Collections.sort(foos, Comparators.fooComparator)
        ...
    }
}

If we directly port to Scala, it looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Foo { ... }

trait Comparator[T] {
    def compare(lhs: T, rhs: T): Int
}

object Collections {
  def sort(collection: Seq[T], comparator: Comparator[T]): Seq[T] = {
    ...
    if (comparator.compare(collection(i), collection(j)) < 0) { ... }
    ...
  }
}

object Comparator {
  val fooComparator = new Comparator[Foo] {
    def compare(lhs: Foo, rhs: Foo): Int = ???
  }
}

object Example extends App {
  val foos = Seq[Foo](???)
  ...
  val sorted = Collections.sort(foos, Comparator.fooComparator)
}

Of course, this functionality is already built into Scala (just as it is built into Java); let’s also adapt it to Scala’s idioms:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
trait Ordering[T] {
    def compare(lhs: T, rhs: T): Int
}

object Ordering {
  val fooOrdering: Ordering[Foo] = new Ordering[Foo] {
    def compare(lhs: Foo, rhs: Foo): Int = ???
  }
}

object Example extends App {
  val foos = new Seq[Foo](???)
  ...
  val sorted = foos.sorted(Ordering.fooOrdering)
}

One thing to notice is that we can drop the separate object for holding the sort function since it now exists (renamed as sorted) mixed into the Seq[Foo] directly.

More significant is the rename of Comparator to Ordering. This shifts focus from a concept of a distinct entity which operates on on the data of type T, to an attribute of type T which happens to be externally defined. That’s one of the major shifts from a Strategy to a Type Class; while both might have the same or similar implementations, type classes are best conceived as an extension of the type rather than something which operates on it.

Implicits

While we could continue to describe Ordering[T] as an attribute extending T, it doesn’t feel much like an extension. To help with that, we can use implicits to select and apply the implementation of Ordering[T] based off of context. To do so, we use the implicit keyword both when the Ordering[Foo] instance is brought into scope, and for the parameter into the sorted function.

To show what that looks like without diving too much into the details of the Scala collection library, we’ll revert back to using a sort function that’s defined externally from the Seq[T]:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
object Ordering {
  implicit val fooOrdering: Ordering[Foo] = ...
}

def sort[T](ts: Seq[T])(implicit ordering: ordering[T]): Seq[T] = ...

object Example extends App {
  import Ordering.fooComparator

  val foos = new Seq[Foo](???)
  ...
   // same as calling `Collections.sort(foos)(fooComparator)`
  val sorted = Seqs.sort(foos)
}

This form of specifying default implicit implementations within the trait’s companion object is very common, so much so that the compiler automatically checks the companion object for an implicit instance of the right type when it can’t find one in scope. This means that the import in the Example can be removed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
object Ordering {
  implicit val fooOrdering: Ordering[Foo] = ...
}

def sort[T](elements: Seq[T])(implicit ordering: Ordering[T]): Seq[T] = {
  ...
  if (ordering.compare(elements(i), elements(j)) < 0) { ... }
  ...
}

object Example extends App {
  val foos = new Seq[Foo](???)
  ...
   // same as calling `Collections.sort(foos)(Ordering.fooComparator)`
  val sorted = Seqs.sort(foos)
}

Context Bound form

While the implicit second parameter list makes calling sort nicer, it still leaves the function definition looking cluttered. In many cases, we want a function which doesn’t reference the implicit implementation directly at all, but still ensures that one is defined, since it calls functions which do need the direct reference:

1
2
3
4
5
6
7
8
9
10
11
12
def sort[T : Ordering](elements: Seq[T]): Seq[T] = {
  val ordering = implicitly[Ordering[T]]
  ...
  if (ordering.compare(elements(i), elements(j)) < 0) { ... }
  ...
}

def processInOrder[T](foos: Seq[T])(implicit ordering: Ordering[T]): Unit = {
  // `ordering` is implicitly passed to `Collections.sort`'s second parameter list
  val sorted = Collections.sort(foos)
  sorted.foreach { ... }
}

In this case the process function can be equivalently rewritten using the Context Bound form of the type parameter:

1
2
3
4
5
6
// the type parameter `T : Ordering` defines an anonymous implicit of type `Ordering[T]`
def processInOrder[T : Ordering](foos: Seq[T]): Unit = {
  // anonymous implicit implementation is available for `Collections.sort`
  val sorted = Collections.sort(foos)
  sorted.foreach { ... }
}

As the name implies, this form is used to ensure that when a function is called, there’s an appropriate implicit implementation defined within the function call’s context. That form could be used when the implicit implementation is used, but some reference to the implicit would need to be acquired. We can use the implicitly keyword to help, as shown in an updated version of the Collections.sort function

1
2
3
4
5
6
def sort[T : Ordering](elements: Seq[T]): Seq[T] = {
  val ordering = implicitly[Ordering[T]]
  ...
  if (ordering.compare(elements(i), elements(j)) < 0) { ... }
  ...
}

or combined in a single line when the implicit reference is only used once:

1
2
3
4
5
def sort[T : Ordering](elements: Seq[T]): Seq[T] = {
  ...
  if (implicitly[Ordering[T]].compare(elements(i), elements(j)) < 0) { ... }
  ...
}

Implicit wrapper

As I mentioned, a type class extends the functionality of a type, external from that type’s definition. Despite that, though, it still feels external rather than an extension. This last mile can be covered by introducing another kind of implicit: implicit classes. These are instances of Adapter wrappers, applied automatically when in scope, to supply method signatures which are not available on unwrapped instances.

In our example, we could have any type which has an implicit Ordering available in context to be wrapped so that we can patch in some methods into the wrapped instance. The most direct one would be a compare method but let’s add compare operators as well:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
implicit class RichOrdering[T : Ordering](underlying: T) {
  def compare(that: T): Int = implicitly[Ordering[T]].compare(underlying, that)
  def <(that: T): Boolean = compare(that) < 0
  def <=(that: T): Boolean = compare(that) <= 0
  def >=(that: T): Boolean = compare(that) >= 0
  def >(that: T): Boolean = compare(that) > 0
}

val first: Foo = ???
val second: Foo = ???

first.compare(second)
first < second
second <= first

Note that in this case we can’t define a == operation, since that’s already defined on all instances as an alias to equals().

Other examples

So far we’ve been focusing on the Comparable / Ordering type but there are a few other common examples available.

Loggable

Most logging frameworks only accept a string message, or an internal Event type with a fairly constrained interface, and the concern of how to pull apart data structures is intermixed with business logic. A type class lets us extract that concern out to a single place which can be reused and not clutter up the main logic.

So when is this useful? Consider, for example, a web server where we want to log requests and responses. Without type classes this might look something like:

1
2
3
4
5
6
7
class SomeHttpServer {
  def handleRequest(request: Request) = {
    log.info(s"Received request.  Session ID: ${request.sessionId}, User ${request.userId}, etc")
    val response: Response = ???
    log.info(s"Responding with HTTP ${response.statusCode}, Contents: ${response.body}, etc")
  }
}

Here the details of what’s being logged is directly in the handleRequest method, somewhat distracting from its purpose and prone to inconsistencies between it and other functions which handle requests. By providing a wrapper to the logger which looks for a Loggable type class, we can extract that concern:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
trait Loggable[A] {
  def format(msg: A): String
}

object Loggable {
  implicit val requestLoggable: Loggable[Request] = new Loggable[Request] {
    def format(msg: Request): String = s"Received request.  Session ID: ${msg.sessionId}, User ${msg.userId}, etc"
  }

  implicit val responseLoggable: Loggable[Response] = new Loggable[Response] {
    def format(msg: Response): String = s"Responding with HTTP ${msg.statusCode}, Contents: ${msg.body}, etc"
  }
}

class WrappedLogger(base: Logger) {
  def debug[A : Loggable](msg: A) = base.debug(implicitly[Loggable[A]].format(msg))
  def info[A : Loggable](msg: A) = base.info(implicitly[Loggable[A]].format(msg))
  def warn[A : Loggable](msg: A) = base.warn(implicitly[Loggable[A]].format(msg))
  def error[A : Loggable](msg: A) = base.error(implicitly[Loggable[A]].format(msg))

  def warn[A : Loggable](msg: A, e: Throwable) = base.warn(implicitly[Loggable[A]].format(msg), e)
  def error[A : Loggable](msg: A, e: Throwable) = base.error(implicitly[Loggable[A]].format(msg), e)
}
1
2
3
4
5
6
7
8
9
10
class SomeHttpServer {
  val log = new WrappedLogger(...)

  def handleRequest(request: Request) = {
    log.info(request)
    val response: Response = ???
    log.info(response)
  }
}

The result: concern for “how to format a request and response” is separated from the main business logic.

Note that in this example we only defined Loggable instances for our Request and Response types so those are the only things which can be logged. Often you’d also want to define a Loggable[String] so you can pass through simple messages, but there are some instances where that might not be desirable.

Going back to the web server example, it’s often useful to have each message related to a request to include a request ID or session ID so that it’s possible to pull out a strand of execution even when servicing multiple requests in parallel. In the JEE world, where each request is tied to a single thread, most logging frameworks provide a Mapped Diagnostic Context (MDC). However, on servers like Netty, which follow the reactive paradigm, multiple threads are expected to handle a request during it’s lifetime, and the additional tracing information would need to be passed around. That could be represented by a RequestContext class, in which case a maybe a Loggable[(RequestContext, String)] would be more appropriate to ensure that relevant information gets logged.

JdbcSettable

If you try to write generic code leveraging JDBC, there’s a tension between code which is as permissive as possible but still constrains types to those which the JDBC API can support. Here type classes extract out common traits, external from the types themselves, to become reusable, generic references:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
trait JdbcSettable[A] {
  def set(stmt: PreparedStatement, index: Int, value: A): Unit
}

object JdbcSettable {
  implicit val stringJdbcSettable = new JdbcSettable[String] {
    def set(stmt: PreparedStatement, index: Int, value: String): Unit = stmt.setString(index, value)
  }

  implicit val intJdbcSettable = new JdbcSettable[Int] {
    def set(stmt: PreparedStatement, index: Int, value: Int): Unit = stmt.setInt(index, value)
  }

  implicit val jodaDateTimeJdbcSettable = new JdbcSettable[DateTime] {
    def set(stmt: PreparedStatement, index: Int, value: DateTime): Unit = stmt.setInt(index, value.toDate)
  }
  ... etc ...
}

implicit class RichStatement[A : JdbcSettable](stmt: PreparedStatement) {
  def set(index: Int, value: A): Unit = implicitly[JdbcSettable[A]].set(stmt, index, value)
}

With that restriction in place, you can define APIs in terms of types you know can interact with with JDBC:

1
2
3
4
5
6
7
8
9
10
// a table of 3 columns
class Table3[A : JdbcSettable, B: JdbcSettable, C : JdbcSettable] {
  def insert(a: A, b: B, c: C): Unit = {
    // ... acquire a Connection and a PreparedStatement ...
    stmt.set(0, a)
    stmt.set(1, b)
    stmt.set(2, c)
    // .. execute the statment, clean up, etc ...
  }
}

Encoders and decoders

This is more of a braod category, but type classes are often used as a way to define mappings from base types to custom user types. Without going too much into the details, one such example is Argonaut which provides the EncodeJson[A] type class for translating an instance of type A into the Json type and its sibling DecodeJson[A] for translating from Json to an instance of A.

In Summary

I think many people find type classes to be a stumbling block as they join the Scala community, in part because of what appears to be magic brought about by implicits and in part because there’s often no mention of how they fit into their existing toolbox. Hopefully this helps to demystify them a little bit; mechanically they’re like Strategies but with more of a focus on declaring a capability (and the operations which go along with that capability) than on extracting the underlying algorithm, with a little bit of implicit pixie dust to help keep the code clean.

Additionally, every new Scala engineer will run across Monads, Functors, Monoids, and Semigroups at some point (plus other concepts as well). While an article can be written on those by themselves, the main thing to keep in mind is that they’re type classes, so they’re just a way of declaring a capability and the related operations. Those just happen to be ones which happen to be fairly generic but still can enable very powerful capabilities (and ones you’ve probably already been using even if you didn’t know it).