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! 🐱