OVO Tech Blog

Cats and type classes

Introduction

Erica Giordo


Cats and type classes

Posted by Erica Giordo on .
Featured

Cats and type classes

Posted by Erica Giordo on .

I love going to Scala meetups and living in London gives you tons of chances to attend lots of them. In the past year there has been a spike in the number of Scala communities, making it easy for everybody to attend. Do you fancy an introductory talk? Do you want to dig deeper into FP? Do you want to talk about new shiny libraries? There's a talk for every taste, there's always pizza and the people are really nice.
I like talking to people at these meetups: everybody has a different background, story and nationality and it great to discuss the talks we're going to attend and what everybody expects to learn from them.

Lately there's has been lot of hype around type classes & Co. and often at the end of the talks I hear: "OMG, they're so cool!! I wanna use them in our project too!". So what's the problem?! Listening to these comments more than once made me realize that there's a huge amount of literature on the web, but maybe it's too scattered. What I'll aim to do here is not trying to reinvent the wheel: there are already great articles around (e.g. the amazing Scala with Cats book written by Noel Welsh and Dave Gurnell or the always-helpful posts from Eugene Yokota), so I'll just try to give you a brief summary of the main type classes used in Cats, each one with an easy example. Like a cheat sheet.
For a more mathematical introduction on the topic you can find another blog post that I wrote here.
Let's start.

Semigroup

A Semigroup is a type class with a combine method:

def combine(x: A, y: A): A

import cats.kernel.Semigroup
import cats.instances.int._
import cats.instances.string._

Semigroup[Int].combine(4, 2) // 6 (for Int, sum is used as combine)
Semigroup[String].combine("Hello ", "world!") // "Hello world!" (while concat is used for strings)

But what if we want to use combine for our own case class? We need to implement our own type class instance:

case class Error(message: String)
object Error {
  implicit val composeError = new Semigroup[Error] {
    override def combine(x: Error, y: Error): Error =
      Error(s"${x.message}, ${y.message}")
  }
}

val firstError = Error("First error")
val secondError = Error("Second error")

Semigroup[Error].combine(firstError, secondError) // Error("First error", "Second error")

Monoid

A Monoid is a Semigroup with a neutral element, that we'll call empty:

def empty: A

import cats.kernel.Monoid
import cats.instances.int._
import cats.instances.string._

Monoid[Int].empty // 0
Monoid[String].empty // ""

In the case of a Monoid, we have an alternative version of combine, called combineAll. Given that a Monoid has an empty element, combineAll is easily defined with a foldLeft:

def combineAll(as: TraversableOnce[A]): A =
  as.foldLeft(empty)(combine)

Monoid[Int].combineAll(List(3, 4, 5)) // 12
Monoid[Int].combineAll(List.empty[Int]) // 0

Monoid[String].combineAll(List("H", "e", "l", "l", "o", "!")) // "Hello!"
Monoid[String].combineAll(List.empty[String]) // ""

Functor

A Functor is a type class with a map method:

def map[A, B](fa: F[A])(f: A => B): F[B]

This is the just map that we all know:

import cats.Functor
import cats.instances.list._
import cats.instances.option._

Functor[List].map(List(1))((x: Int) => x + 1) // List(2)
Functor[Option].map(Option("Hello!"))((x: String) => x.length) // Some(6)

Semigroupal

This is our old Cartesian friend, that with the 1.0 version of Cats had a revamp. Semigroupal is characterized by the following method:

def product[A, B](fa: F[A], fb: F[B]): F[(A, B)]

What product does, is just combining fa and fb. If you remember the cartesian product from your school days, that's exactly what it does (have a look at the List example).

import cats.Semigroupal
import cats.instances.option._
import cats.instances.list._
import cats.syntax.option._

Semigroupal[Option].product(1.some, 2.some) // Some((1, 2))
Semigroupal[Option].product(1.some, none) // None

Semigroupal[List].product(List(1, 2), List(3, 4)) // List((1,3), (1,4), (2,3), (2,4))
Semigroupal[List].product(List.empty, List(3, 4)) // List()

Apply

Apply inherits from Functor and Semigroupal and adds a method ap:

def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]

The definition of the function passed in the Apply context can look weird and discouraging at the beginning, but with an example we can sort it out easily:

import cats.Apply
import cats.syntax.option._
import cats.instances.option._

val ff: Option[String => Int] = Option(_.length)
Apply[Option].ap(ff)("Hello!".some) // Some(6)

A little note: in the previous example we imported import cats.syntax.option._ that, in our case, allows us to lift a String into an Option[String].

Another example with Apply:

import cats.Apply
import cats.instances.list._

val ff: List[Int => Boolean] = List(_ > 2)
Apply[List].ap(ff)(List(0, 1, 2, 3, 4)) // List(false, false, false, true, true)

You could ask yourself "so what's the difference between ap and map?". Well, it's in the signature of the passed function. While map takes A => B, here ap expects a function such as F[A => B].

Applicative

Apply with the addition of the method pure forms an Applicative:

def pure[A](x: A): F[A]

pure just takes an element and lifts it into the Applicative Functor (easy peasy).

import cats.Applicative
import cats.instances.option._
import cats.instances.list._

Applicative[Option].pure(1) // Some(1)
Applicative[List].pure(1) // List(1)

FlatMap

Adding to an Apply the methods flatten and flatMap, we are rewarded with a FlatMap and one step away from a Monad.

def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]

def flatten[A](ffa: F[F[A]]): F[A]

Even if the FlatMap type class is not often mentioned, we always use these two methods.

import cats.FlatMap
import cats.syntax.option._
import cats.instances.option._
import cats.instances.list._

// mouse is imported here just to shake things up a bit 
// (https://github.com/typelevel/mouse)
// given a boolean condition, `.option` will return None if false, 
// otherwise it will return an Option of the argument
//
// e.g. 
// ("Hello".length == 1).option("World") // None
// ("Hello".length != 0).option("World") // Some("World")
import mouse.boolean._

FlatMap[Option].flatten(1.some.some) // Some(1)
FlatMap[List].flatten(List(List(1))) // List(1)

val fOpt = (x: Int) => (x == 1).option(true)
FlatMap[Option].flatMap(none)(fOpt) // None
FlatMap[Option].flatMap(1.some)(fOpt) // Some(true)

val fStr = (x: String) => x.toCharArray.toList
FlatMap[List].flatMap(List.empty)(fStr) // List()
FlatMap[List].flatMap(List("Hello!"))(fStr) // List(H, e, l, l, o, !)

Monad

Finally! What's a Monad then? It is a type class that extends FlatMap and Applicative.

Cheat sheet

Now, as promised:

Type class Inherits from Methods
Semigroup def combine(x: A, y: A): A
Monoid Semigroup def empty: A
Functor def map[A, B](fa: F[A])(f: A => B): F[B]
Semigroupal def product[A, B](fa: F[A], fb: F[B]): F[(A, B)]
Apply Functor
Semigroupal
def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]
Applicative Apply def pure[A](x: A): F[A]
FlatMap Apply def flatten[A](ffa: F[F[A]]): F[A]
def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
Monad FlatMap
Applicative

Conclusion

Type classes and Cats are your friends, don't be scared just because they have fancy names. The Cats documentation is amazing and if you have any doubt while coding have a look at it before opening Stack Overflow (yes, I know, it is THAT amazing, you don't even have to Google your problems - sometimes).
I find the Scala with Cats book really good as an intro for these structures and if you want to practice more on these concepts there's a great website from 47 Degrees that will help you out a lot with that.

Happy coding! 🐱

Erica Giordo

View Comments...