Do you want to understand type classes?
It is sometimes easier to start with a simpler format, this down to earth example of the name printer by Nicolas A Perez will help you. Start with a problem and look at how it can be changed to suit what you need.
'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.
The Name Representation
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.
1 case class Name(first: String, middle: String, last: String)
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.
The Printable Type Class
We can start by defining a trait that represents the type class we are going to use in this example.
1 @typeclass
2 trait Printable[A] {
3 def asString(a: A): String
4 }
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.
1 object BinaryPrinter {
2
3 implicit def fullBinaryPrinter: Printable[Name] = (a: Name) => a.toString
4
5 }
6
7 object FormalNamePrinter {
8
9 implicit val formalPrinter: Printable[Name] = (a: Name) => s"Mr. ${a.last}"
10
11 }
12
13 object LegalNamePrinter {
14
15 implicit val legalPrinter: Printable[Name] = (a: Name) => s"${a.first} ${a.middle}. ${a.last}"
16 }
17
18 object SocialNamePrinter {
19 import LegalNamePrinter._
20
21 implicit val socialPrinter: Printable[Name] = (a: Name) =>
22 if (name.asString == "Nicolas A. Perez")
23 "anicolaspp"
24 else
25 "neo"
26 }
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.
1 def printBinary(name: Name) = {
2 import BinaryPrinter._
3
4 println(name.asString)
5 }
6
7 def printFormal(name: Name) = {
8 implicit val printer = FormalNamePrinter.formalNamePrinter
9
10 println(name.asString)
11 }
12
13 def printLegal(name: Name) = {
14 println(LegalNamePrinter.legalPrinter.asString(name))
15 }
16
17 def printSocial(name: Name) = {
18 import SocialNamePrinter._
19
20 println(name.asString)
21 }
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.
1 val me = Name("Nicolas", "A", "Perez")
2
3 printBinary(me)
4 printFormal(me)
5 printLegal(me)
6 printSocial(me)
7 -------
8 Name(Nicolas,A,Perez)
9 Mr. Perez
10 Nicolas A. Perez
11 anicolaspp
Conclusions
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.'
This article was written by Nicolas A Perez and posted originally on Hackernoon.