Type Classes in Scala
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 anOption[Int]
, which isNone
if the list is empty, and aSome
containing the sum of the members otherwise.
Suppose we want to generalize these functions to work on lists of some other type, such as Double
s, pairs of integers i.e. (Int, Int)
with (x1, x2) + (y1, y2)
defined as (x1 + y1, x2 + y2)
, or pairs of Double
s. 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.