Chapter 01: Type classes
Chapter 1: Introduction
We'll start this chapter by getting familiar with some basic concepts, most of them are abstract Functional Programming concepts that can be seen throughout many languages and tools (especially Haskell) and many are borrowed from Category Theory
. We'll take a quick look upon Category Theory
in later chapters.
TypeClass
A TypeClass
is a tool that helps us to define a common behavior (feature) for a variety of types. In scala, we usually define a TypeClass as a generic trait :
trait StringMaker[T] {
def makeString(t: T): String
}
Now we can define an instance of StringMaker
for any type we want and use the instances wherever we need. In this example, StringMake
is a TypeClass, defined on type T
and knows how to turn t
s into strings. In other words, an instance of StringMaker[X]
adds makeString
behavior to x
s. For example, if we want to have an instance of StringMaker[Int]
we can define it as :
val intStringMaker = new StringMaker[Int]{
def makeString(t: Int): String = t.toString
}
At this point, you may say to yourself that StringMaker
and TypeClasses
are nothing but useless! And you're right since Scala has it's built-in toString
defined in almost every object. But read on and you'll see the magic.
Next, we'll explore some simple examples of TypeClass
es to understand the concept better.
Equal
Let's create a TypeClass called Equal
that does one thing only; checking for equality :
trait Equal[T] {
def eq(left: T, right: T) : Boolean
}
Simple, right? An instance of Equal[X]
knows how to compare two instances of X
together and tell whether they are equal.
Exercise 1.1: Try to write instances of Equal for a couple of simple types like Int and String and then for a case class like Person
Check out the answer after you finished.
Order
Now try to write another TypeClass for object comparison. This one is called Order
, an instance of Order
can not only tell us whether two objects are the same, but it can also tell which one is greater or less than others. We'll
Exercise 1.2: Define TypeClass Order[T] that has these features : 'gt' to check that left is greater than right, 'eq' to check that left and right are equal, 'gte' to check that left is greater or equal to right. Also define 'lt' and 'lte' alike.
Note: How can we use 'Equal' type class that we've defined before?
Also try to play around with 'Order' and make a few instances for it.
Check out the answer after you finished.
JsonParser
Now it's easy to design a simple TypeClass for parsing JSON values. For now, we're just interested in the TypeClass itself, not implementations.
Exercise 1.3: Design the TypeClass 'JsonParser' that provides two functionalities:
1. A function that converts an instance of our desired type to it's corresponding json value (output type would be String)
2. A function that does exact opposite of the previous function: form a String to our desired type
Check out the answer after you finished.
TypeClass instances
By now we have a basic understanding of what a TypeClass is. And also we've seen some TypeClass instance
s. An instance
of a TypeClass is the implementation of that TypeClass for a specific type. TypeClass only defines behaviors and we have to implement the behavior for all our intended types.
So if we want to use a TypeClass on a given type, we need a TypeClass instance for that type, which means we need to implement lots and lots of instances of various TypeClasses for our types, which may cumbersome. But in reality, it isn't that hard, because: First, Scalaz includes lots of TypeClasses and lots of TypeClass instances for common types. Second: One of the main characteristics of Functional Programming is compositionality, which enables us to mix existing TypeClasses or TypeClass instances and invent new ones.
TypeClass syntax
So let's get back to using TypeClasses. Imagine that we need to use the Equal
TypeClass that we've defined in exercise 1.1 to compare people.
val p1: Person = ???
val p2: Person = ???
personEqual.eq(p1, p2)
As you can see, even though using TypeClass instances is easy, it's a little terse, especially if we have to implement peopleAreEqual
function every single time that we need this type class and for each and every type (like Person). So let's try to generalize this function a little bit so that it would work independently of target Type.
def areEqual[T](left: T, right: T)(implicit equal: Equal[T]): Boolean = {
equal.eq(left, right)
}
// Usage:
val p1: Person = ???
val p2: Person = ???
println( areEqual(p1, p2) ) // It's assumed that an implicit instance of Equal[Person] exists in current scope.
It's very simple, right? the only thing that areEqual
does is that it uses an implicit parameter to make working with TypeClasses a little easier(some times this model is called TypeClass interface). Now let us do one final trick:
class EqualOps[T](t: T)(implicit equal: Equal[T]) {
def ===(other: T): Boolean = equal.eq(t, other)
}
object EqualSyntax {
implicit def toEqualOps[T](t: T)(implicit equal: Equal[T]): EqualOps[T] =
new EqualOps(t)
}
So, let's break this code into two parts, EqualOps gets a t
and an instance of Equal[T]
(implicitly of course) and enables dot notation and a cute little operator to ease our lives. So let's assume that we have a t: T
and q: T
that we want to check their equality and we have an implicit instance of Equal[T]
in context, all we have to do is
val eq = new EqualOps(t)
eq === q // or eq.===(q)
But it's still al little terse and also misleading!! This way we push whoever that reads our code to believe that we are checking equality of q
against eq
, not t
!!! Here EqualSyntax
comes to the rescue. the method toEqualsOps
in EqualSyntax
is an implicit conversion from a t: T
into it's corresponding EqualOps[T]
so that we can use equal more easily.
// import an implicit instance of Equal[T]
import EqualSyntax._
t === q
Now that we know how things work to make our life easier, lets write Ops
and Syntax
for Order
TypeClass.
Exercise 1.4: write OrderOps that supports these operations: <, >, <=, >=, === and also named operations for compatibility: gt, lt, gte, lte, eq
Next write OrderSyntax.
Now, scalaz
Now that we know about TypeClasses, we can move on and start using Scalaz. At the core, Scalaz provides lots of useful TypeClasses and various instances for common types. We can easily import Scalaz and start using it.
import scalaz._
import Scalaz._
1 =/= 2 // shouldBe true
1 lte 2 // shouldBe true
List(1, 2) <= List(3) // shouldBe true
12.some > none[Int] // shouldBe true
As you may have guessed, Scalaz provides Order
and Equal
TypeClasses (plus Ops and Syntax) and also nice instances for basic types, collections, functions and many other types for us. Note that 12.some
is a special syntactic sugar provided by Scalaz to ease creating an instance of Option[T]
.
A taste of contramap
We're now equipped with a technique called TypeClass, two TypeClasses (Equal and Order) and instances of these TypeClasses for lots of basic types (Int, String, etc...). But usually, we use complex data structures rather than these basic types, and obviously, we don't have off-the-shelf TypeClass instances for them.
Let's say we have a Person data structure like this:
case class Person(id: String, name: String, age: String) // Person has lots and lots of other properties too.
What if we want an Order
TypeClass instance for the type Person? The first solution that comes to our mind is obvious: Implement an instance! (Remember that we've already defined Equal[Person])
Exercise 1.5: Implement an instance of order TypeClass for Person that orders people based on their age
Note that here we've implemented the instance as a Singleton object instead of a val.
If you look closely at the various implementations of TypeClass instances, a pattern begins to emerge. When we're implementing an instance for a complex data-structure (like Person), sometimes we can map our complex data structure to a simpler one and then use that in our TypeClass instances. For example, in the above exercise, we see Person as just an Integer that represents a person's age. This means that as long as we have an instance of Order TypeClass for Integer, we don't have to write Order[Person]
from scratch. This technique is called Contramap
which we'll deeply investigate in later chapters.
Exercise 1.6(Hard): Try to implement Contramap for our two basic TypeClasses:
def contramapEqual[T,U](f: T => U)(implicit Equal[U]): Equal[T]
def contramapOrder[T,U](f: T => U)(implicit Order[U]): Order[T]
Checkout the contramap for equal here
Checkout the contramap for order here
So, this is another example of composition, we compose simple small structures to create big ones. In the next chapter we start by first building blocks from the magical Category Theory
.
The need for TypeClasses
At this point you might ask yourself "Why bother with TypeClasses?" or "If we want to write equality checker for two type, we'll just write it as a simple function, why writing all this confusing code like TypeClasses or instances or operations?". Honestly, to understand the true power of TypeClasses and Category Theory
, you need to learn some more stuff, especially some patterns and structures (which we'll see soon enough). But the short answer is that these techniques help us create more powerful abstractions. It helps us define structures that are so generic that can be applied to many problems and specific enough that can deliver real value (because normally, the more abstract a structure gets, the less value it can deliver).
To help you understand better, let's think about Order
TypeClass, It's true that when you need to compare two objects of type Person
, you just write something like def isGreaterThan(a: Person, b: Person): Boolean
and never think about TypeClasses and their pain. But with the TypeClass approach you write an abstract code like this:
import OrderSyntax._
def isMax[T](l: List[T], t: T)(implicit order: Order[T]): Boolean = l.forall(_ < t)
Which checks whether a given object t
is greater than everything in the given list l
. It's elegant and beautiful, it's generic and you can use it on any type as long as you have a proper instance of Order
. Although it's a really simple example (which you can easily replicate it without TypeClasses). This pattern of using TypeClasses with implicit parameters is so widely used that Scala has a special syntax for it, you can write the above code as:
import OrderSyntax._
def isMax[T: Order](l: List[T], t: T): Boolean = l.forall(_ < t)
The part T: Order
tells the compiler to ensure there is an instance of Order[T]
available implicitly, so you can omit the implicit parameter.
Last updated
Was this helpful?