Type Classes in Scala
The concept of a type class is borrowed from Haskell and really allows us to push the limits of extensibility and modularity of our types.
It is an elegant way of adding new behaviors to existing types in Scala. We can add multiple functionalities without changing the implementation of the type itself. Let’s dive straight into an example :-
A Simple Example : Finding the Maximum
Let’s say we have the following code to find the Maximum in a list of Integers :-
Now, suppose we are writing an application which manages the admin work for an University . We are tasked with maintaining various statistics for each student like his SAT score and GPA. We also have to maintain the number of papers each professor has published and his years of teaching experience.(The example is a bit contrived and trivial. But I’ve realized it’s easier to explain a concept with simple examples rather than thinking of complex use cases as it allows us to focus on the idea rather than the domain) .
We have the following classes which represent the statistics of a student and professor respectively.
Given a List of StudentStats , how would we find the student with the highest SAT score ? We could of course copy and modify the code we used for a List of Integers and come up with the following :-
How about finding the professor with maximum experience ?
How about finding the Student with the highest GPA ? ….
You get the idea. But , we are duplicating code and there is a lot of repetition.
Duplication is usually the first sign of an abstraction waiting to be discovered.
If you think about it, the only distinct feature across all these functions is the comparison between the corresponding types. If we can define an Ordering on these types and pass it along to our ‘findMax’ function, we can re-use the same findMax function across multiple types.
One way to do this is to have a CustomOrdering interface and make sure there are implementations for ‘each ordering’ that we want to use . We can have multiple ‘orderings’ for the same type as shown here :-
A generic findMax would then look like :-
And we can verify that it works :-
So far, so good. But there is a problem here :-
We need to explicitly pass the ordering we want to use. It would be great if the ordering was passed implicitly and the caller wouldn’t have to worry about passing a specific ordering to findMax.
Here is where Scala’s implicits are of great help. We can pass CustomOrdering as an implicit argument to findMax and make sure that an ordering is in implicit scope when findMax is called.
findMax would then look like :-
We then need to mark the Student and Professor orderings as implicit. :-
And the call would then look like :-
A brief note on implicits
If we hadn’t imported maxByScore in the code above, the Scala compiler would look into the StudentStats companion object by default, but since we have defined two implicit orderings there , it wouldn’t know which one to use.
In cases where only one Ordering makes sense (Integers for example), we can define the implicit in the companion object of the type and the Scala compiler would pick it up without us having to import it.
The Scala compiler will also look in the companion object of the CustomOrdering trait by default, which means that we can also define an implicit ordering there and it will get picked up. This is especially useful if we want to define an Ordering for a type/third party API whose source code we cannot change.
So what is the Type Class Pattern you ask ?
Well, we just implemented it. Surprised ?
Yes, we are already done with it. We defined a type class CustomOrdering
We made StudentStats and ProfessorStats members of the type class CustomOrdering by making sure that values of type CustomOrdering[StudentStats] and CustomOrdering[ProfessorStats] were implicitly available when they were required .
So, any type T can be made a member of a type class TC by making sure that a value of type TC[T] is implicitly available when it is required.
Note that this is different from sub-typing that we are used to in Object Oriented languages.The type T or rather the implementation of type T dosn’t even need to know that it is a member of type class TC. Any type T can be made a member of TC by defining an implicit value of type TC[T]. Thus, we don’t even need access to the source code of T .
Type classes are really useful when we want to extend the behavior of existing types without modifying them. We can see them all over the Scala standard library. They are also heavily used by parsing libraries like Spray Json which can parse any type T to a Json and vice-versa if we make type T a member of a type class defined in their library.
Even the CustomOrdering[T] type class we defined is already defined in the Scala standard library and is called Ordering[T]. The standard library already has default implementations of Ordering[T] for standard types like Int,Double,String etc.
Context Bounds : Syntactic Sugar for Removing Boiler Plate
The following code :-
can also be written as :-
The syntax [T:CustomOrdering] is called a context bound. It tells the compiler that an implicit value of type CustomOrdering[T] will be in scope when findMax is called.
In order to access this value, we need to write implicitly[CustomOrdering[T]] . It fetches an implicit value of type CustomOrdering[T] from the current scope. The definition of implicitly should tell us how it works.
We can even define multiple context bounds :-
Type classes are a great way to write modular and extensible code and are especially useful when you are writing a library that will be used by others. They are heavily used by libraries such as Scalaz, Play and Spray.They allow us to extend the use of an existing functionality to a type hierarchy that probably didn’t even exist when the functionality was implemented .
Do leave a comment with your suggestions :)