Scala with Cats is a book about the Cats library,
which provides abstractions for functional programming in the Scala
programming language. The book provides an introduction not only to the
Cats library but also to some category theory
structures. It’s divided in two major sections: “Theory” and “Case Studies”. The
“Theory” section starts with a chapter dedicated to the way Cats is
designed around type classes and how type classes are encoded in the Scala
programming language. The section follows with dedicated chapters for different
algebraic data structures, some functional programming constructs and how they
are implemented in Cats: Monoids, Semigroups, Functors, Monads, Monad
Transformers, Semigroupal, Applicative, Foldable and Traverse. The “Case
Studies” section ties it all up with 4 practical applications of the previously
introduced structures and constructs: testing asynchronous code, map reduce,
data validation and CRDTs.
I worked through the book in March and April this year and found it engaging and
with a fast pace. Laws are presented and explained in terms of Scala code. The
exercises complement the content of the book well, particularly the ones in the
“Case Studies” section, which showcase the applications of everything that was
introduced in the “Theory” section.
I would recommend the book to anyone with moderate knowledge of the Scala
programming language who wants to learn more about typed functional programming
in general and about the Cats library in particular.
If you’re interested in my solutions to the book’s exercises, they are available
in the following posts:
The following are possible implementations of BoundedSemiLattice for Ints
and Sets. As described in the problem statement, we don’t need to model
non-negativity in the instance for Ints:
Exercise 11.3.3: Generic GCounter
The following is a possible implementation of a generic GCounter that uses
instances of CommutativeMonoid and BoundedSemiLattice:
Exercise 11.4: Abstracting GCounter to a Type Class
The implementation of an instance of the GCounter type class for Map closely
resembles the original GCounter implementation:
Exercise 11.5: Abstracting a Key Value Store
The following is a possible implementation of an instance of the KeyValueStore
type class for Map:
The and method of Check will create a new Check that calls apply on both
instances. However, we soon hit the problem of what to do if they both return a
Left:
We need a way to combine values of type E, which hints towards the need for a
Semigroup instance for E. We’re assuming that we don’t want to short-circuit
but rather accumulate all errors.
For the and implementation, we follow the algebraic data type style that is
recommended by the book:
Validated is a more appropriate data type to accumulate errors than Either.
We can also rely on the Applicative instance for Validated to avoid the
pattern match:
The or combinator should return a Valid if the left hand side is Valid or
if the left hand side is Invalid but the right hand side is Valid. If both
are Invalid, it should return an Invalid combining both errors. Due to the
latter, we can’t rely on orElse but rather have a slightly more complicated
implementation:
Exercise 10.4.2: Checks
With our previous Check renamed to Predicate, we can implement the new
Check with the proposed interface as follows, using an algebraic data type
approach as before:
flatMap is a bit weird to implement because we don’t have a flatMap for
Validated. Fortunately, we have flatMap in Either and a withEither
method in Validated that allows us to apply a function over an Either that
gets converted back to a Validated.
andThen gets implemented very similarly to flatMap, except that we don’t use
the output of the first Check to decide which other Check to use. The next
Check is already statically provided to us:
Exercise 10.4.3: Recap
The helper predicates that are introduced in this exercise make use of a lift
method on Predicate that we haven’t implemented yet. Its implementation can be
something like the following:
A Check for username can be implemented as follows, making use of the
longerThan and alphanumeric predicates.
A Check for the email address can be implemented as follows. We first check
that the string contains at least one @, then split the string, check each of
the sides and combine them back at the end:
Exercise 10.5: Kleislis
The run method on Predicate must return a A => Either[E, A]. We must rely
on the existing apply method so we also need a Semigroup instance for E:
Our checks don’t change much. We have decided to implement the email address
check slightly differently here, applying the checks directly in the split step:
The signature of foldMap can be as follows. We add Monoid as a context bound
for B:
To implement the body of foldMap, we have moved the Monoid from a context
bound to an implicit parameter list for easier access:
On the code above, we have done both steps separetely for clarity (the map and
the foldLeft), but we could have made the calls to func directly in the
combine step of foldLeft.
Exercise 9.3.3: Implementing parallelFoldMap
We can implement parallelFoldMap as follows:
We determine the amount of batches in which to split our work based on the
number of CPUs we have available. We then determine the size of our groups by
dividing the length of our input by the number of batches we’re going to run,
rounding up. We spawn a Future with foldMap for each group and join them via
Future.sequence, reducing the results of each batch with the Monoid instance
we have in scope for B.
Exercise 9.3.4: parallelFoldMap with more Cats
To implement parallelFoldMap using Foldable and Traverse we import the
respective instances from cats.instances.vector and the syntax extension
methods that allow us to call foldMap and traverse directly on Vector
instances:
To write a trait definition for UptimeClient that abstracts over the return
types, we can add a type constructor F[_] as a type parameter:
We can then extend it with two traits that bind F to Future and Id
respectively:
To make sure that the code compiles, we write out the method signatures for
getUptime in each case:
We can now have a TestUptimeClient as a full class based on Map[String, Int]
with no need for relying on Future:
Exercise 8.2: Abstracting over Monads
We can rewrite the method signatures of UptimeService so that it abstracts
over the context as follows:
To get the previous implementation working, we need to not only prove the
compiler that F has an Applicative, but also add a few syntax imports so
that we can call traverse and map: