Toby Hobson

Type classes for beginners

banner.png
Type classes for beginners
Estimated reading time: 8 minutes

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

Type classes are like the adapter pattern on steroids. We can define a contract and make any class implement it, even if the class is final.

You can find complete examples of the concepts discussed on my blog in my Github repo . In particular, check out the type classes example

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

The good news is that type classes in themselves are pretty simple. 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 TrainAdapter(underlying: otherLibrary.Train) extends Vehicle {
  override def drive(): String = "driving a train"
  def brake(): Unit = underlying.brake()
  def derail(): Unit = underlying.derail()
  ...
}

Very nasty!

Why not 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 which takes a Vehicle we can pass any object to it, so long as we have pimped it to make it a Vehicle:

def driveAndReturn(vehicle: Vehicle): Vehicle = ...
val train: Train = new otherLibrary.Train(...)
val drivenTrain: Vehicle = driveAndReturn(train)

The problem is that drivenTrain is no longer a Train, it’s a Vehicle. We can cast it:

val drivenTrain = driveAndReturn(train).asInstanceOf[otherLibrary.Train]

but it’s unsafe and as Scala developers we want to avoid as many runtime exceptions as possible.

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 treated 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”

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 type class (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 Buses, 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 it to accept a List of vehicles:

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

We could keep the existing driveAndReturn 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 and 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("VW"))
val drivenCars: List[Car] = driveAndReturn(cars)

But it gets better, as we have a type class for lists and a type class for buses (and tanks!) we can also pass a list of buses 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 buses. To recap we can handle:

  1. Cars
  2. Buses
  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 boilerplate code by letting the scala compiler wire together the pieces we need.

Wrapping up

The signature for driveAndReturn is a bit verbose as we need to specify the implicit evidence parameter. We can simplify this a bit by using a context bound and the implicitly keyword:

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

Whether this is actually any more concise or readable is subjective

What next

Functor is a simple type class representing the familiar map method. It’s the perfect place to begin your journey with Cats

Semigroup let’s us combine two values recursively. It’s also quite simple to understand and so is a good place to start

comments powered by Disqus

Need help with your project?

Do you need some help or guidance with your project? Reach out to me (email is best)