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 ts into strings. In other words, an instance of StringMaker[X] adds makeString behavior to xs. 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 TypeClasses 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 instances. 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.

Checkout the answer here

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

Checkout the sample test here

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

Check out the answer here

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?