Consider the following functions :

  • sum : which takes a list of integers and returns their sum.

  • sumDifference : which takes two lists of integers and returns the difference of the their sums.

  • sumNonEmpty : which takes a list of integers and returns an Option[Int], which is None if the list is empty, and a Some containing the sum of the members otherwise.

Suppose we want to generalize these functions to work on lists of some other type, such as Doubles, pairs of integers i.e. (Int, Int) with (x1, x2) + (y1, y2) defined as (x1 + y1, x2 + y2), or pairs of Doubles. Take a moment and think about how you would do it. All these types seem to naturally support the operations addition and subtraction, but this fact is not encoded in their inheritance hierarchy, which makes it extremely difficult to write generic functions that work for all of them.

To solve this problem, let’s define a trait called Group :

Group is a generic trait, which takes a type parameter T and declares the operations plus, minus and inverse for objects of type T, as well as an element zero. Note that minus is defined using plus and inverse because x - y = x + (-y)(how clever!).

Groups are a real thing, and the operation plus needs to be associative i.e. (x + y) + z should be equal to x + (y + z), otherwise we may get weird results.

Instances of the trait Group should ideally be defined as implicit members of the object Group, which lives is the same file as the trait. It is called the companion object of the trait. When the Scala compiler needs to supply an implicit parameter of type Group[T], it looks inside the Group companion object. So, we won’t need to pass around Group instances explicitly while writing and using functions that depend on group operations.

Let’s define an instance of the trait for integers (inside the Group object, of course):

The instance for Double looks almost identical, except for the types :

Let’s try something a little more interesting. Here’s a method that produces a an instance of Group for the pair (T1, T2), if it can find instances of Group for T1 and T2 :

This single method will automatically provide Group instances for (Int, Int), (Int, Double), (Double, Int) and (Double, Double) whenever we need them. Isn’t that neat?

Alright, now that we’ve done all the hard work, let’s use the trait to make our sum functions generic :

With these small modifications, you can now use these functions on lists of any type T for which the compiler can find an implicit object of type Group[T]:

That’s it! That’s the type class pattern. Group is called a type class (because it represents a class of types that support group operations), and the types Int, Double, (Int, Int) etc. are called members of the type class Group because the compiler can find instances of the type class for these types.

This post merely scratches the surface of the use cases and benefits of using type classes in Scala. Type classes are awesome and you should use them all over your code base. There are many great resources (much better than this one) for learning about type classes in Scala, like this, this and this. The only reason this post exists is so that I can point out the deficiencies in this naive implementation, and show you how to make it much better in my next post.