Many people coming from Java ask what type classes are, why are they used, and how. This post is not intended to explain how to fully implement type classes, but to show a down to earth example, the name-printer.
In a previous post, Scala Object Serialization for MapR-DB, we used a simple type class to solve the serialization problem we encountered when using MapR-DB from our beloved Scala. Now, we are presenting an even simpler example to show the why and how of type classes.
Let’s start by stating the problem in question. In this case, we have a business object called Name
and it represents, as you might guess, a person’s name. We can represent someone’s name by using the following class.
Now, the question is how do we want to represent this object once we have that need?
For those coming from Java, the answer is simple, we need to override the .toString
function and problem solved. It is, actually, most complex than that.
The same name can be represented differently depending on a specific context. For example, Nicola A Perez
can be Mr. Perez
in a formal context, or it might be Nico
in a very social environment. The exact same name might have to be fully addressed sometimes, and other time (web id) it can just be anicolaspp
.
If we try to attach the representations to the Name
object itself, we certainly are going to be short every time since there will always be a context we did not take in consideration.
The idea is to decouple the object and its possible representations while enabling a polymorphic mechanism to tight them back together when needed, even we if we don’t have access to the original source code of the class itself.
We can start by defining a trait that represents the type class we are going to use in this example.
In the above snippet, we have defined the interface to our possible representations and now let’s see how we could implement some of them.
As we can see, we have added multiple ways to represent a name. Now we can use each of them in a different context. Let’s define some functions where each of them receives a name and prints it. Notice that each function represents the context itself.
It is important to notice that each function knows the context and use it to obtain the right printer in order to represent the name correctly based on the context the function represents.
It is also interesting to see that we added the .asString
function to the name
object. This ad-hoc functionality is only available when corresponding implicit for the given type is in place. The additions happen at compile time and the Scala compiler is able to look at the context in order to choose what type additions are possible.
Finally, we can see how everything plays out together.
Scala powerful type system allows going beyond anything we can do in Java, or for that matter, most programming languages, while keeping type safety at compile time.
In our particular case, type classes also are a very nice and interesting way to add functionality to already existing objects without modifying the original code. In Java, this is partially doable using inheritance, but the problem is not completely solved and in most cases not even possible if types are marked as final.
Ultimately, we are adding functionality on the fly to our objects, and that functionality is only available when we enable the correct context. This, again, is far beyond anything you can do in most programming languages, especially in the type-safe space.