Scala Type classes for beginners

Type classes are everywhere in the Scala ecosystem. If you want to learn advanced libraries like Scalaz, Cats or Shapeless you need to know about them. Even if you don't plan to use these libraries you can (and probably should) use type classes in your own applications. Type classes are borrowed from Haskell and are sometimes called "ad-hoc polymorphism". If you want to know what that means read on ...

I'll try to answer a few key questions in this post:

  • What's wrong with "traditional" polymorphism?
  • What are type classes and why are they better than static polymorphism?
  • What does "composability" mean and why is it important?

While you're reading this post you may think? "What's the big deal? I could do all this without type classes". If so I urge you to read to the end, specifically the section about Composability

TL;DR We write code which can handle a "Car" and a "Bus"; we also write code which can handle a a generic List[A] and Option[A]. We get support for: List[Car], List[Bus], Option[Car], Option[Bus], List[Option[Car]], List[Option[Bus], Option[List[Car]] and Option[List[Bus]] for free!
But type classes give us much more than this!

The good news is that Type classes in themselves are pretty simple, so you can get started quickly. Lets get started ...

The problem with object oriented polymorphism

Lets start with a simple example:

trait Vehicle {  
  def drive(): String 
}

case class Car(make: String) extends Vehicle {  
  override def drive(): String = s"driving a $make car" 
}

case class Bus(color: String) extends Vehicle {  
  override def drive(): String = s"driving a $color bus" 
}

class Person {  
  def drive(vehicle: Vehicle): Unit = println(vehicle.drive())
}

We now decide to use a train drivers library. We'd like to drive a Train but we don't have the source code for the train, only the binaries. We can't modify train to make it extend our Vehicle so we need to subclass the Train:

class MyTrain extends otherLibrary.Train with Vehicle {  
  override def drive(): String = "driving a train"
}

It's a bit nasty but it kind of works. But what if otherLibrary.Train is declared to be final? We need an adapter:

class MyTrain(underlying: otherLibrary.Train) extends Vehicle {  
  override def drive(): String = "driving a train"
  def derail(): Unit = underlying.derail()
  ...
}

Very nasty!

Implicit classes

Actually it doesn't need to be so nasty. Implicit classes, introduced in Scala 2.10 allow us to pimp classes and add our own behaviour:

implicit class MyTrain(underlying: otherLibrary.Train) extends Vehicle {  
  override def drive(): String = "driving a train"
}

If we have a function accepting a vehicle we can pass any object to it, so long as we have pimped it to make it a train:

def driveAndReturn(vehicle: Vehicle): Vehicle = ...  
val car: Car = Car("ford")  
val drivenCar: Vehicle = driveAndReturn(car)  

The problem is that drivenCar is no longer a car, it's a Vehicle. We can cast it: val drivenCar = driveAndReturn(car).asInstanceOf[Car] but it's really hacky.

What are type classes?

In "classic" object oriented design we describe behaviour in an interface/trait and define concrete implementations of that interface. In other words we say a Car is a Vehicle. Type classes approach this from a slightly different angle. We define a trait which says something of type A should be capable of being used as a Vehicle. We then create concrete implementations of this contract for each type of Vehicle we are interested in. If it sounds complex, an example will clear it up:

trait VehicleLike[A] {  
  def drive(a: A): String
}

Notice how we paramaterize the trait and the drive method now takes a parameter of type A. An implementation for a Car and Bus would look like:

val vehicleLikeCar = new VehicleLike[Car] {  
  def drive(car: Car): String = s"driving a ${car.make} car"
}

val vehicleLikeBus = new VehicleLike[Bus] {  
  def drive(bus: Bus): String = s"driving a ${bus.color} bus"
}

we can now drive a car and bus :

val ford = Car("ford")  
val londonBus = Bus("red")  
println(vehicleLikeCar.drive(ford))  
println(vehicleLikeBus.drive(londonBus))  

so what's the big deal? We're not finished yet. Let's make the implementations available implicitly and write a generic method that would like to "drive" something:

implicit val vehicleLikeCar = ...  
implicit val vehicleLikeBus = ...

def driveAndReturn[A](vehicle: A)(implicit evidence: VehicleLike[A]): A = {  
  println(evidence.drive(vehicle)); vehicle
}

Take a moment to think about this, driveAndReturn can handle anything, it doesn't care. By convention we name the implicit parameter "evidence" or "ev". The method is saying "pass me what you like, but you have to supply evidence that I can drive it" Crucially whatever type we pass in we get back:

val ford: Car = Car("ford")  
// still a Car
val drivenFord: Car = driveAndReturn(ford)  

A typical Scala library will define the typeclass (trait) and functions that use it, VehicleLike and driveAndReturn in our case along with some common implementations e.g. vehicleLikeCar and vehicleLikeBus. A typical pattern would be something like:

trait VehicleLike[A] {  
  def drive(a: A): String
}

object Vehicles {  
  implicit val vehicleLikeCar = ...  
  implicit val vehicleLikeBus = ...
}

object Dealership {  
  def driveAndReturn[A](vehicle: A)(implicit ev: VehicleLike[A]): A = ...
}

callers would use the library by importing the implementations before calling the method in question:

import Vehicles._ // import car and bus implementations

val ford = Car("ford")  
Dealership.driveAndReturn(ford) // vehicleLikeCar is already in scope due to the import  

But here is the real power - callers of this method are not limited to cars and busses, they can write their own type class instances:

implicit val vehicleLikeTank = new VehicleLike[Tank] {  
  def drive: String = "driving a big big tank"
}

val tank = Tank(...)  
val drivenTank: Tank = Dealership.driveAndReturn(tank)  

Type classes compose

Lets say we want to pass a list of cars to driveAndReturn we have a few options:

We could modify driveAndReturn to accept a list of vehicles:

def driveAndReturn[A](vehicles: List[A])(implicit vehicleLike: VehicleLike[A]): List[A] = ...  

We could keep the existing signature but create a type class implementation for a List of Cars:

implicit val VehicleLikeCarList = new VehicleLike[List[Car]] {  
  def drive(cars: List[Car]): String =
    cars.map(car => s"driving a ${car.make} car").mkString(System.lineSeparator())
}

Neither solution is ideal. Let's compose two type classes

We will leave the original vehicleLikeCar unchanged but create a new type class for Lists. But we won't create it for List[Car] but List[A]. Our new type class will behave in a similar way to driveAndReturn, it will also look for a type class implementation for A:

implicit def vehicleListList[A](implicit ev: VehicleLike[A]) = new VehicleLike[List[A]] {  
  override def drive(aa: List[A]): String =
    aa.map(a => ev.drive(a)).mkString(System.lineSeparator())
}

Notice we use a def not a val. Instead of creating a type class instance we're actually creating a factory which builds a type class for List[A] by first looking for a type class for A

Our driveAndReturn method stays unchanged but we can now pass a list of cars to it:

val cars = List(Car("ford"), Car("BMW"), Car("VM"))  
val drivenCars: List[Car] = driveAndReturn(cars)  

But it gets better, as we have a type class for lists and a type class for busses (and tanks!) we can also pass a list of busses to the method. Lets summarise and show all the code together:

trait VehicleLike[A] {  
  def drive(a: A): String
}

implicit val vehicleLikeCar = new VehicleLike[Car] {  
  def drive(car: Car): String = s"driving a ${car.make} car"
}

implicit val vehicleLikeBus = new VehicleLike[Bus] {  
  def drive(bus: Bus): String = s"driving a ${bus.color} bus"
}

implicit def vehicleListList[A](implicit evidence: VehicleLike[A]) = new VehicleLike[List[A]] {  
  override def drive(aa: List[A]): String =
    aa.map(a => evidence.drive(a)).mkString(System.lineSeparator())
}

def driveAndReturn[A](vehicle: A)(implicit evidence: VehicleLike[A]): A = {  
  println(evidence.drive(vehicle)); vehicle
}

val cars = List(Car("ford"), Car("BMW"), Car("VM"))  
val busses = List(Bus("red"), Bus("yellow"))

val drivenFord: Car = driveAndReturn(cars.head)  
val drivenCars: List[Car] = driveAndReturn(cars)

val drivenBus: Bus = driveAndReturn(busses.head)  
val drivenBusses: List[Bus] = driveAndReturn(busses)  

Options - Still with me? Lets create a type class for Option[A]:

implicit def vehicleLikeOption[A](implicit ev: VehicleLike[A]) = new VehicleLike[Option[A]] {  
  override def drive(a: Option[A]) = a.map(ev.drive).getOrElse("Nothing to drive")
}

val someCar: Option[Car] = Some(Car("ford"))  
val noCar: Option[Car] = None

driveAndReturn(someCar)  
driveAndReturn(noCar)  

Of course we can now also handle optional Busses. To recap we can handle:

  1. Cars
  2. Busses
  3. Lists
  4. Options

Question - Can we handle a List[Option[Car]] ? Lets try:

val listOfOptionalCars: List[Option[Car]] = List(Some(Car("ford")), Some(Car("VW")), None)  
driveAndReturn(listOfOptionalCars)  

The answer is we can! And we can flip the List and Option and it will still work:

val optionalListOfCars: Option[List[Car]] = Some(List(Car("ford")))  
driveAndReturn(optionalListOfCars)  

This is composability in action and it's what makes type classes so powerful. We can dramatically cut down on boiletplate code by letting the scala compiler wire together the pieces we need.