Advanced Scala
Advanced Scala
Advanced Scala
with Cats
underscore
Foreword 15
Changelog . . . . . . . . . . . . . . . . . . . . . . . . 16
Omissions . . . . . . . . . . . . . . . . . . . . . . . . 16
Source Code . . . . . . . . . . . . . . . . . . . . . . . 17
Callout Boxes . . . . . . . . . . . . . . . . . . . . . . . 18
Acknowledgements . . . . . . . . . . . . . . . . . . . . . . 19
I Theory 21
1 Introduc on 23
1.1.3 Interfaces . . . . . . . . . . . . . . . . . . . . . 25
3
4 CONTENTS
3 Functors 55
4 Monads 77
The aims of this book are two-fold: to introduce monads, functors, and
other func onal programming pa erns as a way to structure program
design, and to explain how these concepts are implemented in Cats.
Monads, and related concepts, are the func onal programming equiva-
lent of object-oriented design pa ernsarchitectural building blocks
that turn up over and over again in code. They dier from object-
oriented pa erns in two main ways:
15
16 CONTENTS
This book is in early access status. This means there are unnished
aspects as detailed below. There may be typos and errata in the
text and examples.
Changelog
Star ng from the March 2016 release, here are the major changes to
the book:
Omissions
New terms and phrases are introduced in italics. A er their ini al intro-
duc on they are wri en in normal roman font.
Source Code
Most code passes through tut to ensure it compiles. tut uses the Scala
console behind the scenes, so we some mes wrap code in an object to
account for dierences between the console and regular code:
object example {
sealed trait Foo[A]
final case class Bar[A](a: A) extends Foo[A]
Callout Boxes
Acknowledgements
Theory
21
Chapter 1
Introduc on
Cats contains a wide variety of func onal programming tools and allows
developers to pick and choose the ones we want to use. The majority
of these tools are delivered in the form of type classes that we can apply
to exis ng Scala types.
Type classes are a programming pa ern origina ng in Haskell. They al-
low us to extend exis ng libraries with new func onality, without using
tradi onal inheritance, and without altering the original library source
code.
In this chapter we will refresh our memory of type classes from Under-
scores Essen al Scala book, and take a rst look at the Cats codebase.
We will look at two example type classesShow and Equsing them to
iden fy pa erns that lay the founda ons for the rest of the book.
There are three important components to the type class pa ern: the
type class itself, instances for par cular types, and the interface methods
that we expose to users.
23
24 CHAPTER 1. INTRODUCTION
The instances of a type class provide implementa ons for the types we
care about, including types from the Scala standard library and types
from our domain model.
In Scala we dene instances by crea ng concrete implementa ons of
the type class and tagging them with the implicit keyword:
object JsonWriterInstances {
implicit val stringJsonWriter = new JsonWriter[String] {
def write(value: String): Json =
JsString(value)
}
implicit val personJsonWriter = new JsonWriter[Person] {
def write(value: Person): Json =
JsObject(Map(
1.1. ANATOMY OF A TYPE CLASS 25
1.1.3 Interfaces
Interface Objects
object Json {
def toJson[A](value: A)(implicit w: JsonWriter[A]): Json =
w.write(value)
}
To use this object, we import any type class instances we care about
and call the relevant method:
import JsonWriterInstances._
Json.toJson(Person("Dave", "[email protected]"))
// res4: Json = JsObject(Map(name -> JsString(Dave), email ->
JsString([email protected])))
Interface Syntax
26 CHAPTER 1. INTRODUCTION
import JsonWriterInstances._
import JsonSyntax._
Person("Dave", "[email protected]").toJson
// res5: Json = JsObject(Map(name -> JsString(Dave), email ->
JsString([email protected])))
3. Finally, use the type class on the console or in a short demo app:
create a Cat and print it to the console:
28 CHAPTER 1. INTRODUCTION
// Define a cat:
val cat = Cat(/* ... */)
4. Use the extension methods to print the example Cat you created
in the previous exercise.
In the next sec on we will take a rst look at Cats. We will examine
the standard code layout Cats uses to organize its type classes, and see
how to select type classes, instances, and syntax for use in our code.
The type classes in Cats are dened in the cats package. We can import
Show directly from this package:
30 CHAPTER 1. INTRODUCTION
import cats.Show
The companion object of every Cats type class has an apply method
that locates an instance for any type we specify:
cats.instances.int Int
cats.instances.string String
cats.instances.list List
cats.instances.option Option
cats.instances.map Map and subtypes
cats.instances.all All instances
and so on See the
cats.instances
package for more
1.2. MEET CATS 31
Thats be er! We now have access to two instances of Show, and can
use them to print Ints and Strings:
val intAsString: String =
showInt.show(123)
// intAsString: String = 123
We can make Show easier to use by impor ng the interface syntax from
cats.syntax.show. This adds a show method to any type for which
we have an instance of Show in scope:
import cats.syntax.show._
Cats provides separate syntax imports for each type class. We will in-
troduce these as we encounter them in later sec ons and chapters.
import java.util.Date
These constructors exist for Show but dont make sense for all Cats type
classes. We will introduce constructors for other type classes as we
come to then.
Re-implement the Cat applica on from the previous sec on using Show
instead of Printable.
Cats type classes are dened in the cats package. For example, the
Show type class is dened as cats.Show.
1.3 Example: Eq
trait Eq[A] {
def eqv(a: A, b: A): Boolean
// other concrete methods based on eqv...
}
eqInt.eqv(123, 234)
// res2: Boolean = false
import cats.instances.int._
import cats.instances.option._
import cats.syntax.option._
import java.util.Date
import cats.instances.long._
x === x
// res11: Boolean = true
x === y
// res12: Boolean = false
Use this to compare the following pairs of objects for equality and in-
equality:
val cat1 = Cat("Garfield", 35, "orange and black")
val cat2 = Cat("Heathcliff", 30, "orange and black")
1.4 Summary
The type classes themselves are generic traits in the cats pack-
age.
In the remaining chapters of this book we will look at four broad and
powerful type classesMonoids, Functors, Monads, Applicatives,
and more. In each case we will learn what func onality the type class
provides, the formal rules it follows, and how it is implemented in Cats.
Many of these type classes are more abstract than Show or Eq. While
this makes them harder to learn, it makes them far more useful for solv-
ing general problems in our code.
Chapter 2
In this sec on we explore our rst type classes, monoid and semigroup.
These allow us to add or combine values. There are instances for Ints,
Strings, Lists, Options, and many, many more. Lets start by looking
at a few simple types and opera ons to see what common principles
we can extract.
Integer addi on
2 + 1
// res0: Int = 3
2 + 0
// res1: Int = 2
39
40 CHAPTER 2. MONOIDS AND SEMIGROUPS
0 + 2
// res2: Int = 2
There are also other proper es of addi on. For instance, it doesnt mat-
ter in what order we add elements because we always get the same
result. This is a property known as associa vity:
(1 + 2) + 3
// res3: Int = 6
1 + (2 + 3)
// res4: Int = 6
The same proper es for addi on also apply for mul plica on, provided
wee use 1 as the iden ty instead of 0:
1 * 3
// res5: Int = 3
3 * 1
// res6: Int = 3
(1 * 2) * 3
// res7: Int = 6
1 * (2 * 3)
// res8: Int = 6
We can also add Strings, using string concatena on as our binary op-
erator:
2.1. DEFINITION OF A MONOID 41
"One" ++ "two"
// res9: String = Onetwo
"" ++ "Hello"
// res10: String = Hello
"Hello" ++ ""
// res11: String = Hello
Note that we used ++ above instead of the more usual + to suggest a par-
allel with sequences. We can do exactly the same with other types of
sequence, using concatena on as as the binary operator and the empty
sequence as our iden ty.
trait Monoid[A] {
def combine(x: A, y: A): A
def empty: A
}
def identityLaw[A](x: A)
(implicit m: Monoid[A]): Boolean = {
(m.combine(x, m.empty) == x) &&
(m.combine(m.empty, x) == x)
}
1 - (2 - 3)
// res16: Int = 2
In prac ce we only need to think about laws when we are wri ng our
own Monoid instances for custom data types. Most of the me we can
rely on the instances provided by Cats and assume the library authors
know what theyre doing.
not dene an empty element. For example, we have just seen that se-
quence concatena on and integer addi on are monoids. However, if
we restrict ourselves to non-empty sequences and posi ve integers, we
lose access to an empty element. Cats has a NonEmptyList data type
that has an implementa on of Semigroup but no implementa on of
Monoid.
trait Semigroup[A] {
def combine(x: A, y: A): A
}
Weve seen a few examples of monoids but there are plenty more to
be found. Consider Boolean. How many monoids can you dene for
this type? For each monoid, dene the combine and empty opera ons
and convince yourself that the monoid laws hold. Use the following
deni ons as a star ng point:
trait Semigroup[A] {
def combine(x: A, y: A): A
}
44 CHAPTER 2. MONOIDS AND SEMIGROUPS
object Monoid {
def apply[A](implicit monoid: Monoid[A]) =
monoid
}
Now weve seen what a monoid is, lets look at their implementa on in
Cats. Once again well look at the three main aspects of the implemen-
ta on: the type class, the instances, and the interface.
import cats.Monoid
import cats.Semigroup
Cats Kernel?
The Cats Kernel type classes covered in this book are Eq,
Semigroup, and Monoid. All the other type classes we cover are
part of the main Cats project and are dened directly in the cats
package.
Monoid follows the standard Cats pa ern for the user interface: the
companion object has an apply method that returns the type class in-
stance. So if we wanted the monoid instance for String, and we have
the correct implicits in scope, we can write the following:
import cats.Monoid
import cats.instances.string._
Monoid[String].empty
// res1: String = ""
Monoid.apply[String].empty
// res3: String = ""
The type class instances for Monoid are organised under cats.instances
in the standard way described in Chapter 1. For example, if we want
to pull in instances for Int we import from cats.instances.int:
import cats.Monoid
import cats.instances.int._
Monoid[Int].combine(32, 10)
// res5: Int = 42
val a = Option(22)
// a: Option[Int] = Some(22)
val b = Option(20)
2.5. MONOIDS IN CATS 47
// b: Option[Int] = Some(20)
Monoid[Option[Int]].combine(a, b)
// res6: Option[Int] = Some(42)
Cats provides syntax for the combine method in the form of the |+|
operator. Because combine technically comes from Semigroup, we ac-
cess the syntax by impor ng from cats.syntax.semigroup:
import cats.syntax.semigroup._
import cats.instances.string._
import cats.instances.int._
Well done! SuperAdders market share con nues to grow, and now
there is demand for addi onal func onality. People now want to add
48 CHAPTER 2. MONOIDS AND SEMIGROUPS
We need to release this code really soon so we cant make any modi-
ca ons to add. Make it so!
See the solu on
When working with type classes we must consider two issues that con-
trol instance selec on:
When we dene type classes we can add variance annota ons to the
type parameter like we can for any other generic type. To quickly recap,
there are three cases:
When the compiler searches for an implicit it looks for one matching
the type or subtype. Thus we can use variance annota ons to control
type class instance selec on to some extent.
There are two issues that tend to arise. Lets imagine we have an alge-
braic data type like:
sealed trait A
final case object B extends A
final case object C extends A
It turns out we cant have both at once. The three choices give us be-
haviour as follows:
Contravariant
Type Class Variance Invariant Covariant
Its clear there is no perfect system. Cats generally prefers to use in-
variant type classes. This allows us to specify more specic instances
for subtypes if we want. It does mean that if we have, for example, a
value of type Some[Int], our monoid instance for Option will not be
used. We can solve this problem with a type annota on like Some(1)
: Option[Int] or by using smart constructors that construct values
with the type of the base trait in an algebraic data type. For example,
Cats provides some and none constructors for Option:
import cats.instances.option._
import cats.syntax.option._
Some(1)
// res0: Some[Int] = Some(1)
1.some
// res1: Option[Int] = Some(1)
None
// res2: None.type = None
2.7. APPLICATIONS OF MONOIDS 51
none[Int]
// res3: Option[Int] = None
The other issue is choosing between type class instances when several
are available for a specic type. For example, how do we select the
monoid for integer mul plica on instead of the monoid for integer ad-
di on?
Cats currently has no mechanism for selec ng alterna ve instances,
though this may change in the future.
We can always dene or import a type class instance into the local
scope. This will take precedence over other type class instances in the
implicit scope:
import cats.Monoid
import cats.syntax.semigroup._
3 |+| 2
// res5: Int = 6
In big data applica ons like Spark and Hadoop we distribute data anal-
ysis over many machines, giving fault tolerance and scalability. This
means each machine will return results over a por on of the data, and
we must then combine these results to get our nal result. In the vast
majority of cases this can be viewed as a monoid.
If we want to calculate how many total visitors a web site has received,
that means calcula ng an Int on each por on of the data. We know the
monoid instance of Int is addi on, which is the right way to combine
par al results.
If we want to calculate 99% and 95% response mes from our server
logs, we can use a data structure called a QTree for which there is a
monoid.
Hopefully you get the idea. Almost every analysis that we might want
to do over a large data set is a monoid, and therefore we can build
an expressive and powerful analy cs system around this idea. This is
exactly what Twi ers Algebird and Summingbird projects have done.
We explore this idea further in the Map-Reduce case study.
A par cular class of data types support this reconcilia on. These data
types are called commuta ve replicated data types (CRDTs). The key
opera on is the ability to merge two data instances, with a result that
captures all the informa on in both instances. This opera on relies on
having a monoid instance. We explore this idea further in the CRDT
case study.
The two examples above are cases where monoids inform the en re
system architecture. There are also many cases where having a monoid
around makes it easier to write a small code fragment. Well see lots of
examples in the case studies in this book.
2.8 Summary
We hit a big milestone in this chapterwe covered our rst type classes
with fancy func onal programming names:
import cats.Monoid
import cats.instances.all._
import cats.syntax.semigroup._
With these three things in scope, we can set about adding anything we
want:
54 CHAPTER 2. MONOIDS AND SEMIGROUPS
Functors
Lets start as we did with monoids by looking at a few types and opera-
ons and seeing what general principles we can abstract.
Sequences
The map method is perhaps the most commonly used method on List.
If we have a List[A] and a func on A => B, map will create a List[B].
55
56 CHAPTER 3. FUNCTORS
There are some proper es of map that we rely on without even think-
ing about them. For example, we expect the two snippets below to
produce the same output:
In general, the map method for a List works like this: We start with a
List[A] of length n, we supply a func on from A to B, and we end up
with a List[B] of length n. The elements are changed but the ordering
and length of the list are preserved. This is illustrated in Figure 3.1.
map
Op ons
Option(1).map(_.toString)
// res3: Option[String] = Some(1)
Option(123).map(_ * 4).map(_ + 4)
// res4: Option[Int] = Some(496)
Option(123).map(x => (x * 2) + 4)
// res5: Option[Int] = Some(250)
In general, the map method for an Option works similarly to that for a
List. We start with an Option[A] that is either a Some[A] or a None,
we supply a func on from A to B, and the result is either a Some[B] or
a None. Again, the structure is preserved: if we start with a Some we
end up with a Some, and a None always maps to a None. This is shown
in Figure 3.2.
map
Lets expand how we think about map by taking some other examples
into account:
Futures
Some func onal purists disagree with this because the excep on handling in
Scala futures breaks the functor laws. Were going to ignore this detail because real
func onal programs dont do excep ons.
58 CHAPTER 3. FUNCTORS
Await.result(future1, 1.second)
// res6: String = Hello world!
Await.result(future2, 1.second)
// res7: Int = 12
map
Can we map over func ons of a single argument? What would this
mean?
All our examples above have had the following general shape:
A func on with a single argument has two types: the parameter type
and the result type. To get them to the same shape we can x the
parameter type and let the result type vary:
We can see this with our trusty type chart in Figure 3.4.
map
map
Intui vely, a functor F[A] represents some data (the A type) in a context
(the F type). The map opera on modies the data within but retains the
structure of the surrounding context. To ensure this is the case, the
following laws must hold:
Iden ty: calling map with the iden ty func on is the same as doing noth-
ing:
fa.map(a => a) == fa
Composi on: mapping with two func ons f and g is the same as
mapping with f and then mapping with g:
fa.map(g(f(_))) == fa.map(f).map(g)
import scala.language.higherKinds
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
If you havent seen syntax like F[_] before, its me to take a brief de-
tour to discuss type constructors and higher kinded types. Well explain
that scala.language import as well.
Kinds are like types for types. They describe the number of holes in
a type. We dis nguish between regular types that have no holes, and
type constructors that have holes that we can ll to produce types.
For example, List is a type constructor with one hole. We ll that hole
by specifying a parameter to produce a regular type like List[Int]
or List[A]. The trick is not to confuse type constructors with generic
types. List is a type constructor, List[A] is a type:
Theres a close analogy here with func ons and values. Func ons are
value constructorsthey produce values when we supply parameters:
Kind nota on
// ...
}
Armed with this knowledge of type constructors, we can see that the
Cats deni on of Functor allows us to create instances for any single-
parameter type constructor, such as List, Option, or Future.
3.5. FUNCTORS IN CATS 63
import scala.language.higherKinds
import cats.Functor
import cats.instances.list._
import cats.instances.option._
lifted(Option(1))
// res0: Option[Int] = Some(2)
The main method provided by the syntax for Functor is map. Its dif-
cult to demonstrate this with Options and Lists as they have their
own built-in map opera ons. If there is a built-in method it will always
be called in preference to an extension method. Instead we will use
func ons as our example:
import cats.instances.function._
import cats.syntax.functor._
func3(123)
// res1: Int = 248
value.map(func)
}
Write a Functor for the following binary tree data type. Verify that the
code works as expected on instances of Branch and Leaf:
Were now going to look at two other type classes, one that represents
prepending opera ons to a chain, and one that represents building a
bidirec onal chain of opera ons.
One great use case for these new type classes is building libraries that
transform, read, and write values. The content es in ghtly to the
JSON codec case study later in the book.
3.6. CONTRAVARIANT AND INVARIANT FUNCTORS 67
The rst of our type classes, the contravariant functor, provides an oper-
a on called contramap that represents prepending a transforma on
to a chain. This is illustrated in Figure 3.6.
contramap
Well talk about contramap itself directly for now, bringing in the type
class in a moment.
The contramap method only makes sense for certain data types. For
example, we cant dene contramap for an Option because there is no
way of feeding a value in an Option[B] backwards through a func on
A => B.
contramap starts to make sense when we have a data types that repre-
sent tranforma ons. For example, consider the Printable type class
we discussed in Chapter 2:
trait Printable[A] {
def format(value: A): String
}
trait Printable[A] {
def format(value: A): String
format("hello")
// res4: String = "hello"
format(true)
3.6. CONTRAVARIANT AND INVARIANT FUNCTORS 69
Dene an instance of Printable that prints the value from this case
class:
Rather than wri ng out the complete deni on from scratch (new
Printable[Box] etc), create your instance using the contramap
method of one of the instances above.
See the solu on
Your instance should work as follows:
format(Box("hello world"))
// res6: String = "hello world"
format(Box(true))
// res7: String = yes
format(Box(123))
// <console>:19: error: could not find implicit value for
parameter p: Printable[Box[Int]]
// format(Box(123))
// ^
The second of our type classes, the invariant functor, provides a method
called imap that is informally equivalent to a combina on of map and
contramap. We can demonstrate this by extending Printable to pro-
duce a typeclass for encoding and decoding to a String:
70 CHAPTER 3. FUNCTORS
trait Codec[A] {
def encode(value: A): String
def decode(value: String): Option[A]
imap
,
encode(Box(123))
// res13: String = 123
decode[Box[Int]]("123")
// res14: Option[Box[Int]] = Some(Box(123))
Co- and contravariant functors capture the same principle without the
limita ons of subtyping. As we said above subtyping can be viewed as
72 CHAPTER 3. FUNCTORS
trait Invariant[F[_]] {
def imap[A, B](fa: F[A])(f: A => B)(g: B => A): F[B]
}
ignored. Cats uses this to provide opera ons that work with any of
the three types of functor.
import cats.Show
import cats.functor.Contravariant
import cats.instances.string._
showSymbol.show('dave)
// res2: String = 'dave
import cats.instances.function._
import cats.syntax.contravariant._
div2.contramap(add1)(2)
// res4: Double = 1.5
One example is the tupled method provided by the cartesian builder syntax dis-
cussed in Chapter 6.
74 CHAPTER 3. FUNCTORS
import cats.Semigroup
import cats.instances.string._ // semigroup for String
import cats.syntax.invariant._ // imap extension method
import cats.syntax.semigroup._
3.8 Summary
Regular Functors are by far the most common of these type classes,
but even then is rare to use them on their own. They form the build-
ing block of several more interes ng abstrac ons that we use all the
me. In the following chapters we will look at two of these abstrac-
ons: Monads and Applicatives.
3.8. SUMMARY 75
The map method for collec on types is important because each element
in a collec on can be transformed independently of the rest. This al-
lows us to parallelise or distribute transforma ons on large collec ons,
a technique leveraged heavily in map reduce frameworks like Hadoop.
We will inves gate this approach in more detail in the Pygmy Hadoop
case study later in the book.
The Contravariant and Invariant type classes are more situa onal.
We wont be doing much more work with them, although we will revisit
them to discuss Cartesians, and for the JSON Codec case study later
in the book.
76 CHAPTER 3. FUNCTORS
Chapter 4
Monads
Monads are one of the most common abstrac ons in Scala. Many Scala
programmers quickly become intui vely familiar with monads, even if
we dont know them by name.
In this chapter we will take a deep dive into monads. We will start by
mo va ng them with a few examples. Well proceed to their formal
deni on and their implementa on in Cats. Finally, well tour some
interes ng monads that you may not have heard of, providing introduc-
ons and examples of their use.
77
78 CHAPTER 4. MONADS
This is the ques on that has been posed in a thousand blog posts, with
explana ons and analogies involving concepts as diverse as cats, mexi-
can food, and monoids in the category of endofunctors (whatever they
are). Were going to solve the problem of explaining monads once and
for all by sta ng very simply:
That was easy! Problem solved, right? Ok, maybe we need some more
discussion
Informally, the most important feature of a monad is its flatMap
method, which allows us to specify what happens next. This is what
we mean by sequencing computa ons. A monad allows us to specify
a sequence of opera ons that happen one a er another. We specify
the applica on-specic part of the computa on as a func on param-
eter, and flatMap runs our func on and takes care of some kind of
complica on (conven onally referred to as an eect). Lets ground
things by looking at some examples.
Op ons
Each of these computa ons may fail, as indicated by their Option re-
turn types. The flatMap method on Option allows us to sequence
4.1. WHAT IS A MONAD? 79
these opera ons without having to constantly check whether they re-
turn Some or None:
At each step, flatMap chooses whether to call our func on, and our
func on generates the next computa on in the sequence. This is
shown in Figure 4.1.
flatMap
stringDivideBy("6", "2")
// res1: Option[Int] = Some(3)
stringDivideBy("6", "0")
// res2: Option[Int] = None
stringDivideBy("6", "foo")
// res3: Option[Int] = None
stringDivideBy("bar", "2")
// res4: Option[Int] = None
Every monad is also a functor (see below for proof), so we can rely on
both flatMap and map to sequence computa ons that do and and dont
introduce a new monad. Plus, if we have both flatMap and map we can
use for comprehensions to clarify the sequencing behaviour:
Lists
When we rst encounter flatMap as budding Scala developers, we
tend to think of it as a pa ern for itera ng over Lists. This is rein-
forced by the syntax of for comprehensions, which look very much like
impera ve for loops:
for {
x <- numbersBetween(1, 3)
y <- numbersBetween(4, 5)
} yield (x, y)
4.1. WHAT IS A MONAD? 81
flatMap
Futures
Future is a monad that allows us to sequence computa ons without
worrying that they are asynchronous:
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
Again, we specify the code to run at each step, and flatMap takes care
of all the horrifying underlying complexi es of thread pools and sched-
ulers.
If youve made extensive use of Scalas Futures, youll know that the
code above is fetching trac from each server in sequence. This be-
comes clearer if we expand out the for comprehension to show the
nested calls to flatMap:
flatMap
While we have only talked about the flatMap method above, the
monad behaviour is formally captured in two opera ons:
import scala.language.higherKinds
trait Monad[F[_]] {
def pure[A](value: A): F[A]
In some languages and libraries, notably Haskell and Scalaz, flatMap is referred
to as bind. This is purely a dierence in terminology. Well use the term flatMap for
compa bility with Cats and the Scala standard library.
84 CHAPTER 4. MONADS
Importantly, the pure and flatMap methods must obey three laws:
Le iden ty: calling pure then transforming the result with a func on
f is the same as simply calling f:
pure(a).flatMap(f) == f(a)
Right iden ty: passing pure to flatMap is the same as doing nothing:
m.flatMap(pure) == m
Associa vity: flatMapping over two func ons f and g is the same as
flatMapping over f and then flatMapping over g:
import scala.language.higherKinds
trait Monad[F[_]] {
def pure[A](a: A): F[A]
Here are some examples using pure and flatMap, and map directly:
import cats.Monad
import cats.instances.option._
import cats.instances.list._
Monad provides many other methods as well, including all of the meth-
ods from Functor. See the scaladoc for more informa on.
Cats provides instances for all the monads in the standard library
(Option, List, Vector and so on) via cats.instances:
import cats.instances.option._
import cats.instances.list._
import cats.instances.vector._
val fm = Monad[Future]
// <console>:37: error: could not find implicit value for
parameter instance: cats.Monad[scala.concurrent.Future]
// val fm = Monad[Future]
// ^
import scala.concurrent.ExecutionContext.Implicits.global
val fm = Monad[Future]
// fm: cats.Monad[scala.concurrent.Future] = cats.instances.
FutureInstances$$anon$1@1c54d4ad
Await.result(
fm.flatMap(fm.pure(1)) { x =>
fm.pure(x + 2)
},
1.second
)
// res3: Int = 3
import cats.syntax.applicative._
import cats.instances.option._
import cats.instances.list._
1.pure[Option]
// res4: Option[Int] = Some(1)
1.pure[List]
// res5: List[Int] = List(1)
import scala.language.higherKinds
import cats.Monad
import cats.syntax.functor._
import cats.syntax.flatMap._
import cats.instances.option._
import cats.instances.list._
sumSquare(Option(3), Option(4))
// res8: Option[Int] = Some(25)
4.3. THE IDENTITY MONAD 89
We can rewrite this code using for comprehensions. The Scala compiler
will do the right thing by rewri ng our comprehension in terms of
flatMap and map and inser ng the correct implicit conversions to use
our Monad:
def sumSquare[M[_] : Monad](a: M[Int], b: M[Int]): M[Int] =
for {
x <- a
y <- b
} yield x*x + y*y
sumSquare(Option(3), Option(4))
// res10: Option[Int] = Some(25)
import scala.language.higherKinds
import cats.Monad
import cats.syntax.functor._
import cats.syntax.flatMap._
x <- a
y <- b
} yield x*x + y*y
This method works well on Options and Lists but we cant call it pass-
ing in plain values:
sumSquare(3, 4)
// <console>:22: error: no type parameters for method sumSquare:
(a: M[Int], b: M[Int])(implicit evidence$1: cats.Monad[M])M
[Int] exist so that it can be applied to arguments (Int, Int
)
// --- because ---
// argument expression's type is not compatible with formal
parameter type;
// found : Int
// required: ?M[Int]
// sumSquare(3, 4)
// ^
// <console>:22: error: type mismatch;
// found : Int(3)
// required: M[Int]
// sumSquare(3, 4)
// ^
// <console>:22: error: type mismatch;
// found : Int(4)
// required: M[Int]
// sumSquare(3, 4)
// ^
import cats.Id
4.3. THE IDENTITY MONAD 91
Now we can call our monadic method using plain values. However, the
exact seman cs are dicult to understand. We cast the parameters to
sumSquare as Id[Int] and received an Int back as a result!
type Id[A] = A
123 : Id[Int]
// res4: cats.Id[Int] = 123
List(1, 2, 3) : Id[List[Int]]
// res5: cats.Id[List[Int]] = List(1, 2, 3)
val b = Monad[Id].flatMap(a)(_ + 1)
// b: cats.Id[Int] = 4
import cats.syntax.flatMap._
import cats.syntax.functor._
92 CHAPTER 4. MONADS
for {
x <- a
y <- b
} yield x + y
// res6: cats.Id[Int] = 7
// In production:
Await.result(sumSquare(Future(3), Future(4)), 1.second)
// res8: Int = 25
// In test:
sumSquare(a, b)
// res10: cats.Id[Int] = 25
Implement pure, map, and flatMap for Id! What interes ng discover-
ies do you uncover about the implementa on?
See the solu on
4.4 Either
Lets look at another useful monadic data type. The Scala standard li-
brary has a type Either. In Scala 2.11 and earlier, Either wasnt tech-
4.4. EITHER 93
for {
a <- either1.right
b <- either2.right
} yield a + b
// res4: scala.util.Either[String,Int] = Right(444)
In Scala 2.12, Either was redesigned. The modern Either makes the
decision that the right side is always the success case and thus supports
map and flatMap directly. This turns Either into a monad and makes
working with it much more pleasant:
for {
a <- either1
b <- either2
} yield a + b
// res5: scala.util.Either[String,Int] = Right(444)
import cats.syntax.either._
val a = 3.asRight[String]
// a: Either[String,Int] = Right(3)
val b = 4.asRight[String]
// b: Either[String,Int] = Right(4)
for {
x <- a
y <- b
4.4. EITHER 95
There are two problems here, both arising because the compiler
chooses the type of accumulator based on the rst parameter
list to foldRight:
countPositive(List(1, 2, 3))
// res7: Either[String,Int] = Right(3)
Either.catchOnly[NumberFormatException]("foo".toInt)
Either.catchNonFatal(sys.error("Badness"))
There are also methods for crea ng an Either from other data types:
Either.fromTry(scala.util.Try("foo".toInt))
Either.fromOption[String, Int](None, "Badness")
4.4. EITHER 97
import cats.syntax.either._
"Error".asLeft[Int].getOrElse(0)
// res9: Int = 0
"Error".asLeft[Int].orElse(2.asRight[String])
// res10: Either[String,Int] = Right(2)
"error".asLeft[String] recover {
case str: String =>
"Recovered from " + str
}
// res12: Either[String,String] = Right(Recovered from error)
"error".asLeft[String] recoverWith {
case str: String =>
Right("Recovered from " + str)
}
// res13: Either[String,String] = Right(Recovered from error)
"foo".asLeft[Int].leftMap(_.reverse)
// res14: Either[String,Int] = Left(oof)
6.asRight[String].bimap(_.reverse, _ * 7)
// res15: Either[String,Int] = Right(42)
"bar".asLeft[Int].bimap(_.reverse, _ * 7)
// res16: Either[String,Int] = Left(rab)
123.asRight[String]
// res17: Either[String,Int] = Right(123)
123.asRight[String].swap
// res18: scala.util.Either[Int,String] = Left(123)
for {
a <- 1.asRight[String]
b <- 0.asRight[String]
c <- if(b == 0) "DIV0".asLeft[Int] else (a / b).asRight[String
]
} yield c * 100
// res19: scala.util.Either[String,Int] = Left(DIV0)
4.4. EITHER 99
When using Either for error handling, we need to determine what type
we want to use to represent errors. We could use Throwable for this
as follows:
result1.fold(handleError, println)
// User(dave,passw0rd)
result2.fold(handleError, println)
// User not found: dave
Is the error handling strategy in the previous exercises well suited for
all purposes? What other features might we want from error handling?
x // first access
// res0: Int = 2
x // second access
// res1: Int = 2
By contrast, defs are lazy and not memoized. The code to compute y
below is not run un l we access it (lazy), and is re-run on every access
(not memoized):
102 CHAPTER 4. MONADS
def y = {
println("Computing Y")
1 + 1
}
// y: Int
y // first access
// Computing Y
// res2: Int = 2
y // second access
// Computing Y
// res3: Int = 2
Last but not least, lazy vals are lazy and memoized. The code to
compute z below is not run un l we access it for the rst me (lazy). The
result is then cached and re-used on subsequent accesses (memoized):
lazy val z = {
println("Computing Z")
1 + 1
}
// z: Int = <lazy>
z // first access
// Computing Z
// res4: Int = 2
z // second access
// res5: Int = 2
import cats.Eval
// import cats.Eval
now.value
// res6: Int = 3
later.value
// res7: Int = 7
always.value
// res8: Int = 11
Each type of Eval calculates its result using one of the evalua on mod-
els dened above. Eval.now captures a value right now. Its seman cs
are similar to a valeager and memoized:
val x = Eval.now {
println("Computing X")
1 + 1
}
// Computing X
// x: cats.Eval[Int] = Now(2)
val y = Eval.always {
println("Computing Y")
1 + 1
}
// y: cats.Eval[Int] = cats.Always@253eef84
val z = Eval.later {
println("Computing Z")
1 + 1
}
// z: cats.Eval[Int] = cats.Later@a81d4d5
Eager Lazy
Eval's map and flatMap methods add computa ons to a chain. This is
similar to the map and flatMap methods on scala.concurrent.Future,
except that the computa ons arent run un l we call value to obtain
a result:
val greeting = Eval.always {
println("Step 1")
"Hello"
}.map { str =>
println("Step 2")
str + " world"
}
// greeting: cats.Eval[String] = cats.Eval$$anon$8@3c88d726
greeting.value
// Step 1
// Step 2
// res15: String = Hello world
Note that, while the seman cs of the origina ng Eval instances are
maintained, mapping func ons are always called lazily on demand (def
seman cs):
a + b
}
// Calculating A
// ans: cats.Eval[Int] = cats.Eval$$anon$8@9284e40
One useful property of Eval is that its map and flatMap methods are
trampolined. This means we can nest calls to map and flatMap arbitrar-
ily without consuming stack frames. We call this property stack safety.
factorial(50000)
// java.lang.StackOverflowError
// ...
factorial(50000).value
// java.lang.StackOverflowError
// ...
Oops! That didnt workour stack s ll blew up! This is because were
s ll making all the recursive calls to factorial before we start working
with Eval's map method. We can work around this using Eval.defer,
which takes an exis ng instance of Eval and defers its evalua on un l
later. defer is trampolined like Eval's map and flatMap methods, so
we can use it as a way to quickly make an exis ng opera on stack safe:
108 CHAPTER 4. MONADS
factorial(50000).value
// res20: BigInt =
33473205095971448369154760940714864779127732238104548077301003219901680221
Eval is a useful tool to enforce stack when working on very large com-
puta ons and data structures. However, we must bear in mind that
trampolining is not free. It avoids consuming stack by crea ng a chain
of func on calls on the heap. There are s ll limits on how deeply we
can nest computa ons, but they are bounded by the size of the heap
rather than the stack.
Writer is the rst data type weve seen from the [cats.data]
package. This package provides numerous data types: instances
of various type classes that produce useful seman cs. Other ex-
amples from cats.data include the monad transformers that we
will see in the next chapter, and the Validated type that we will
see in Chapter 6.
import cats.data.Writer
import cats.instances.vector._
Writer(Vector(
"It was the best of times",
"It was the worst of times"
), 123)
// res0: cats.data.WriterT[cats.Id,scala.collection.immutable.
Vector[String],Int] = WriterT((Vector(It was the best of
110 CHAPTER 4. MONADS
123.pure[Logged]
// res2: Logged[Int] = WriterT((Vector(),123))
import cats.syntax.writer._
We can extract the result and log from a Writer using the value and
written methods respec vely:
a.value
// res4: cats.Id[Int] = 123
a.written
// res5: cats.Id[scala.collection.immutable.Vector[String]] =
Vector(msg1, msg2, msg3)
or we can extract log and result at the same me using the run method:
val (log, result) = b.run
// log: scala.collection.immutable.Vector[String] = Vector(msg1,
msg2, msg3)
// result: Int = 123
to use a log type that has an ecient append and concatenate opera-
ons, such as a Vector:
val writer1 = for {
a <- 10.pure[Logged]
_ <- Vector("a", "b", "c").tell
b <- 32.writer(Vector("x", "y", "z"))
} yield a + b
// writer1: cats.data.WriterT[cats.Id,Vector[String],Int] =
WriterT((Vector(a, b, c, x, y, z),42))
writer1.run
// res6: cats.Id[(Vector[String], Int)] = (Vector(a, b, c, x, y,
z),42)
writer2.run
// res7: cats.Id[(scala.collection.immutable.Vector[String], Int
)] = (Vector(A, B, C, X, Y, Z),42)
writer3.run
// res8: cats.Id[(scala.collection.immutable.Vector[String], Int
)] = (Vector(A, B, C, X, Y, Z),4200)
writer4.run
// res9: cats.Id[(scala.collection.immutable.Vector[String], Int
)] = (Vector(a!, b!, c!, x!, y!, z!),42000)
Finally, we can clear the log with the reset method and swap log and
result with the swap method:
writer5.run
// res10: cats.Id[(Vector[String], Int)] = (Vector(),42)
writer6.run
// res11: cats.Id[(Int, Vector[String])] = (42,Vector(a, b, c, x
, y, z))
114 CHAPTER 4. MONADS
Writers are useful for logging opera ons in mul -threaded environ-
ments. Lets conrm this by compu ng (and logging) some factorials.
The factorial func on below computes a factorial, prin ng out the
intermediate steps in the calcula on as it runs. The slowly helper func-
on ensures this takes a while to run, even on the very small examples
we need in this book to t the output on the page:
factorial(5)
// fact 0 1
// fact 1 1
// fact 2 2
// fact 3 6
// fact 4 24
// fact 5 120
// res13: Int = 120
import scala.concurrent._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
4.7. THE READER MONAD 115
Await.result(Future.sequence(Vector(
Future(factorial(3)),
Future(factorial(3))
)), 5.seconds)
// fact 0 1
// fact 0 1
// fact 1 1
// fact 1 1
// fact 2 2
// fact 2 2
// fact 3 6
// fact 3 6
// res14: scala.collection.immutable.Vector[Int] =
// Vector(120, 120)
import cats.data.Reader
We can extract the func on again using the Reader's run method and
call it using apply as usual:
catName.run(Cat("Garfield", "lasagne"))
// res0: cats.Id[String] = Garfield
We can create Readers from func ons and extract the func ons again.
So far so simple, but what advantage do Readers give us over the raw
func ons?
The power of Readers comes from their map and flatMap methods,
which represent dierent kinds of func on composi on. The general
pa ern of usage is to create a set of Readers that accept the same
type of congura on, combine them with map and flatMap, and then
call run to inject the cong at the end.
The map method simply extends the computa on in the Reader by pass-
ing its result through a func on:
4.7. THE READER MONAD 117
greetAndFeed(Cat("Garfield", "lasagne"))
// res3: cats.Id[String] = Hello Garfield Have a nice bowl of
lasagne
def checkPassword(
username: String,
password: String
): DbReader[Boolean] = ???
def checkLogin(
userId: Int,
password: String
): DbReader[Boolean] = ???
val db = Db(
Map(
1 -> "dade",
2 -> "kate",
3 -> "margo"
),
Map(
"dade" -> "zerocool",
"kate" -> "acidburn",
"margo" -> "secret"
)
)
// db: Db = Db(Map(1 -> dade, 2 -> kate, 3 -> margo),Map(dade ->
zerocool, kate -> acidburn, margo -> secret))
checkLogin(1, "zerocool").run(db)
// res8: cats.Id[Boolean] = true
checkLogin(4, "davinci").run(db)
// res9: cats.Id[Boolean] = false
As you can hopefully see from the exercise, Readers eec vely pro-
vide a simple tool for doing dependency injec on. We write steps of
our program as instances of Reader, chain them together with map and
flatMap, and build a func on that accepts the dependency as input.
Kleisli arrows
You may have no ced from console output in this sec on that
Reader is implemented in terms of another type called Kleisli.
Kleisli arrows provide a more general form of Reader that gener-
alise over the type constructor of the result type.
Kleislis are beyond the scope of this book, but will be easy to pick
up based on your newfound knowledge of Reader and the con-
tent well cover in the next chapter on monad transformers.
import cats.data.State
As weve seen with Reader and Writer, the power of the State
monad comes from combining instances. The map and flatMap
methods thread the State from one instance to another. Because
each primi ve instance represents a transforma on on the state, the
combined instance represents a more complex transforma on.
val step1 = State[Int, String] { num =>
val ans = num + 1
(ans, s"Result of step1: $ans")
}
// step1: cats.data.State[Int,String] = cats.data.StateT@e680f25
As you can see, in this example the nal state is the result of applying
both transforma ons in sequence. The state is threaded from step to
step even though we dont interact with it in the for comprehension.
4.8. THE STATE MONAD 123
The general model for using the State monad, then, is to represent
each step of a computa on as an instance of State, and compose the
steps using the standard monad operators. Cats provides several con-
venience constructors for crea ng primi ve steps:
getDemo.run(10).value
// res3: (Int, Int) = (10,10)
setDemo.run(10).value
// res4: (Int, Unit) = (30,())
pureDemo.run(10).value
// res5: (Int, String) = (10,Result)
inspectDemo.run(10).value
// res6: (Int, String) = (10,10!)
124 CHAPTER 4. MONADS
modifyDemo.run(10).value
// res7: (Int, Unit) = (11,())
import State._
1 2 +
We can write a simple interpreter for these expressions using the State
monad. We can parse each symbol into a State instance represen ng
a context-free stack transform and intermediate result. The State in-
stances can be threaded together using flatMap to produce an inter-
preter for any sequence of symbols.
126 CHAPTER 4. MONADS
Lets do this now. Start by wri ng a func on evalOne that parses a sin-
gle symbol into an instance of State. Use the code below as a template.
Dont worry about error handling for nowif the stack is in the wrong
congura on, its ok to throw an excep on and fail.
import cats.data.State
If this seems dicult, think about the basic form of the State instances
youre returning. Each instance represents a func onal transforma on
from a stack to a pair of a stack and a result. You can ignore any wider
context and focus on just that one step:
evalOne("42").runA(Nil).value
// res3: Int = 42
program.runA(Nil).value
// res4: Int = 3
program.runA(Nil).value
program.runA(Nil).value
import cats.Monad
import scala.annotation.tailrec
val optionMonad =
new Monad[Option] {
def flatMap[A, B](opt: Option[A])
(fn: A => Option[B]): Option[B] =
opt flatMap fn
@tailrec
def tailRecM[A, B](a: A)
(fn: A => Option[Either[A, B]]): Option[B] =
fn(a) match {
case None => None
case Some(Left(a1)) => tailRecM(a1)(fn)
case Some(Right(b)) => Some(b)
}
}
Lets write a Monad for our Tree data type from last chapter. Heres the
type again, together with the smart constructors we used to simplify
type class instance selec on:
Verify that the code works on instances of Branch and Leaf, and that
the Monad provides Functor-like behaviour for free.
4.10 Summary
In this chapter weve also seen some of the custom types and data
structures that Cats provides, including Id, Reader, Writer, and State.
These cover a wide range of uses and many problems can be solved by
using one of these constructs.
Monad Transformers
Monads are like burritos, which means that once you acquire a taste,
youll nd yourself returning to them again and again. This is not with-
out issues. As burritos can bloat the waist, monads can bloat the code
base through nested for-comprehensions.
Imagine we are interac ng with a database. We want to look up a
user record. The user may or may not be present, so we return an
Option[User]. Our communica on with the database could fail for
many reasons (network issues, authen ca on problems, database prob-
lems, and so on), so this result is wrapped up in an Either, giving us a
nal result of Either[Error, Option[User]].
To use this value we must nest flatMap calls (or equivalently,
for-comprehensions):
131
132 CHAPTER 5. MONAD TRANSFORMERS
new Monad[Composed] {
def pure[A](a: A): Composed[A] =
a.pure[M2].pure[M1]
Heres an example that uses OptionT to squash List and Option into
a single monad. Where we might use List[Option[A]] we can use
ListOption[A] to avoid nested flatMap calls.
import cats.data.OptionT
import cats.Monad
import cats.instances.list._
import cats.syntax.applicative._
val a = 10.pure[ListOption]
// a: ListOption[Int] = OptionT(List(Some(10)))
val b = 32.pure[ListOption]
// b: ListOption[Int] = OptionT(List(Some(32)))
This is the basics of using monad transformers. The combined map and
flatMap methods allow us to use both component monads without
having to recursively unpack and repack values at each stage in the
computa on. Now lets look at the API in more depth.
Complexity of imports
By conven on, in Cats a monad Foo will have a transformer class called
FooT. In fact, many monads in Cats are dened by combining a monad
transformer with the Id monad. Concretely, some of the available in-
stances are:
All of these monad transformers follow the same conven on: the rst
type parameter species the monad that is wrapped around the monad
implied by the transformer. The remaining type parameters are the
types weve used to form the corresponding monads.
136 CHAPTER 5. MONAD TRANSFORMERS
Kleisli Arrows
We can now reveal that Kleisli and ReaderT are, in fact, the
same thing! ReaderT is actually a type alias for Kleisli. Hence
why we were crea ng Readers last chapter and seeing Kleislis
on the console.
import cats.instances.either._
Now lets add another monad into our stack. Lets create a Future of
an Either of Option. Once again we build this from the inside out with
an OptionT of an EitherT of Future. However, we cant dene this in
one line because EitherT has three type parameters:
import scala.concurrent.Future
import cats.data.EitherT
import scala.concurrent.Future
import cats.data.{EitherT, OptionT}
Our mammoth stack composes not two but three monads. Our map and
flatMap methods cut through three layers of abstrac on:
import scala.concurrent.ExecutionContext.Implicits.global
import cats.instances.future._
Kind Projector
import cats.instances.option._
// import cats.instances.option._
Kind Projector cant simplify all type declara ons down to a single
line, but it can reduce the number of intermediate type deni ons
we need to keep our code readable.
5.2. MONAD TRANSFORMERS IN CATS 139
As we saw above, we can use pure to directly inject raw values into
transformed monad stacks. We can also create instances from untrans-
formed stacks using the monad transformers apply method:
stack1.value
// res13: ErrorOr[Option[Int]] = Right(Some(123))
stack2.value
// res14: ErrorOr[Option[Int]] = Right(Some(123))
import cats.instances.vector._
import cats.data.{Writer, EitherT, OptionT}
Two autobots can perform a special move if their combined power level
is greater than 15. Write a second method, canSpecialMove, that ac-
cepts the names of two allies and checks whether a special move is
possible. If either ally is unavailable, fail with an appropriate error mes-
sage:
def canSpecialMove(
ally1: String,
ally2: String
): Response[Boolean] = ???
Finally, write a method tacticalReport that takes two ally names and
prints a message saying whether they can perform a special move:
def tacticalReport(
ally1: String,
ally2: String
): String = ???
tacticalReport("Jazz", "Bumblebee")
// res25: String = Jazz and Bumblebee need a recharge.
tacticalReport("Jazz", "Ironhide")
// res27: String = Comms error: Ironhide unreachable
5.4 Summary
import cats.syntax.either._
// import cats.syntax.either._
val a = Option(1.asRight[String])
// a: Option[Either[String,Int]] = Some(Right(1))
val b = Option(1.asRight[String])
// b: Option[Either[String,Int]] = Some(Right(1))
vides the code needed to merge its related monad with other monads.
The transformer is a data structure that wraps a monad stack, equipping
it with map and flatMap methods that unpack and repack the whole
stack:
import cats.data.EitherT
import cats.instances.option._
The type signatures of monad transformers are wri en from the in-
side out, so an EitherT[Option, String, A] is a wrapper for an
Option[Either[String, A]]. It is o en useful to use type aliases
when wri ng transformer types for deeply nested monads.
import cats.syntax.either._
for {
a <- parseInt("a")
b <- parseInt("b")
c <- parseInt("c")
} yield (a + b + c)
147
148 CHAPTER 6. CARTESIANS AND APPLICATIVES
import scala.concurrent._
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
Await.result(timestamps, 1.second)
// res5: (Long, Long, Long) = (0,106,210)
map and flatMap arent quite capable of capturing what we want here
because they make the assump on that each computa on is dependent
on the previous one:
6.1 Cartesian
import cats.Cartesian
import cats.instances.option._ // Cartesian for Option
Cartesian[Option].product(Some(123), Some("abc"))
// res0: Option[(Int, String)] = Some((123,abc))
Cartesian[Option].product(None, Some("abc"))
// res1: Option[(Nothing, String)] = None
Cartesian[Option].product(Some(123), None)
// res2: Option[(Int, Nothing)] = None
Cartesian.map3(
Option(1),
Option(2),
Option(3)
)(_ + _ + _)
// res5: Option[Int] = Some(6)
Cartesian.map3(
Option(1),
Option(2),
Option.empty[Int]
)(_ + _ + _)
// res6: Option[Int] = None
import cats.instances.option._
import cats.syntax.cartesian._
builder2.tupled
// res8: Option[(Int, String)] = Some((123,abc))
builder3.tupled
// res9: Option[(Int, String, Boolean)] = Some((123,abc,true))
builder5.tupled
// res10: Option[(Int, String, Boolean, Double, Char)] = Some
((123,abc,true,0.5,x))
(
Option(1) |@|
Option(2) |@|
Option(3)
).tupled
// res11: Option[(Int, Int, Int)] = Some((1,2,3))
(
Option("Garfield") |@|
Option(1978) |@|
Option("Orange and black")
).map(Cat.apply)
// res12: Option[Cat] = Some(Cat(Garfield,1978,Orange and black)
)
Cartesian builders also have a contramap and imap methods that ac-
cept Contravariant and Invariant functors. For example, we can com-
bine Monoids and Semigroups using Invariant. Heres an example:
154 CHAPTER 6. CARTESIANS AND APPLICATIVES
import cats.Monoid
import cats.instances.boolean._
import cats.instances.int._
import cats.instances.list._
import cats.instances.string._
import cats.syntax.cartesian._
Our Monoid allows us to create empty Cats and add Cats together
using the syntax from Chapter 2:
import cats.syntax.monoid._
Monoid[Cat].empty
// res18: Cat = Cat(,0,List())
The seman cs for Future are pre y much what wed expect, providing
parallel as opposed to sequen al execu on:
import scala.concurrent._
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
import cats.Cartesian
import cats.instances.future._
Await.result(futurePair, 1.second)
// res2: (String, Int) = (Hello,123)
The two Futures start execu ng the moment we create them, so they
are already calcula ng results by the me we call product. Cartesian
builder syntax provides a concise syntax for zipping xed numbers of
Futures:
import cats.syntax.cartesian._
val futureCat = (
Future("Garfield") |@|
Future(1978) |@|
Future(List("Lasagne"))
).map(Cat.apply)
Await.result(futureCat, 1.second)
// res5: Cat = Cat(Garfield,1978,List(Lasagne))
There is a Cartesian instance for List. What value do you think the
following expression will produce?
import cats.Cartesian
import cats.instances.list._
1. product could zip the lists, returning List((1, 3), (2, 4));
The name Cartesian is a hint as to which answer well get, but lets
run the code to be sure:
6.3. CARTESIAN APPLIED TO DIFFERENT TYPES 157
import cats.instances.either._
Cartesian[ErrorOr].product(
Left(Vector("Error 1")),
Left(Vector("Error 2"))
)
// res10: ErrorOr[(Nothing, Nothing)] = Left(Vector(Error 1))
The reason for these surprising results is that, like Option, List and
Either are both monads. To ensure consistent seman cs, Cats Monad
(which extends Cartesian) provides a standard deni on of product
in terms of map and flatMap.
import scala.language.higherKinds
import cats.Monad
We can implement product in terms of the monad opera ons, and Cats
enforces this implementa on for all monads. This gives what we might
think of as unexpected and less useful behaviour for a number of data
types. The consistency of seman cs is actually useful for higher level
abstrac ons, but we dont know about those yet.
So why bother with Cartesian at all? The answer is that we can create
useful data types that have instances of Cartesian (and Applicative)
but not Monad. This frees us to implement product in dierent ways.
Lets examing this further by looking at a new data type for error han-
dling.
6.4 Validated
import cats.Cartesian
import cats.data.Validated
import cats.instances.list._ // Semigroup for List
Cartesian[AllErrorsOr].product(
Validated.invalid(List("Error 1")),
Validated.invalid(List("Error 2"))
)
// res1: AllErrorsOr[(Nothing, Nothing)] = Invalid(List(Error 1,
Error 2))
val v = Validated.Valid(123)
// v: cats.data.Validated.Valid[Int] = Valid(123)
val i = Validated.Invalid("Badness")
// i: cats.data.Validated.Invalid[String] = Invalid(Badness)
import cats.syntax.validated._
123.valid[String]
// res2: cats.data.Validated[String,Int] = Valid(123)
"Badness".invalid[Int]
// res3: cats.data.Validated[String,Int] = Invalid(Badness)
Validated.catchOnly[NumberFormatException]("foo".toInt)
// res4: cats.data.Validated[NumberFormatException,Int] =
Invalid(java.lang.NumberFormatException: For input string: "
foo")
Validated.catchNonFatal(sys.error("Badness"))
// res5: cats.data.Validated[Throwable,Nothing] = Invalid(java.
lang.RuntimeException: Badness)
Validated.fromTry(scala.util.Try("foo".toInt))
// res6: cats.data.Validated[Throwable,Int] = Invalid(java.lang.
NumberFormatException: For input string: "foo")
Validated.fromEither[String, Int](Left("Badness"))
// res7: cats.data.Validated[String,Int] = Invalid(Badness)
Cartesian[AllErrorsOr]
// <console>:22: error: could not find implicit value for
parameter instance: cats.Cartesian[AllErrorsOr]
// Cartesian[AllErrorsOr]
// ^
import cats.instances.string._
Cartesian[AllErrorsOr]
// res10: cats.Cartesian[AllErrorsOr] = cats.data.
ValidatedInstances$$anon$1@5e0fe145
import cats.syntax.cartesian._
(
"Error 1".invalid[Int] |@|
"Error 2".invalid[Int]
).tupled
// res11: cats.data.Validated[String,(Int, Int)] = Invalid(Error
1Error 2)
As you can see, String isnt an ideal type for accumula ng errors. We
commonly use Lists or Vectors instead:
import cats.instances.vector._
(
Vector(404).invalid[Int] |@|
Vector(500).invalid[Int]
).tupled
// res12: cats.data.Validated[scala.collection.immutable.Vector[
Int],(Int, Int)] = Invalid(Vector(404, 500))
(
NonEmptyVector.of("Error 1").invalid[Int] |@|
NonEmptyVector.of("Error 2").invalid[Int]
).tupled
// res13: cats.data.Validated[cats.data.NonEmptyVector[String],(
Int, Int)] = Invalid(NonEmptyVector(Error 1, Error 2))
123.valid.map(_ * 100)
// res14: cats.data.Validated[Nothing,Int] = Valid(12300)
"?".invalid.leftMap(_.toString)
// res15: cats.data.Validated[String,Nothing] = Invalid(?)
"Badness".invalid[Int]
// res18: cats.data.Validated[String,Int] = Invalid(Badness)
"Badness".invalid[Int].toEither
// res19: Either[String,Int] = Left(Badness)
"Badness".invalid[Int].toEither.toValidated
// res20: cats.data.Validated[String,Int] = Invalid(Badness)
As with Either, we can use the ensure method to fail with a specied
error if a predicate does not hold:
164 CHAPTER 6. CARTESIANS AND APPLICATIVES
// 123.valid[String].ensure("Negative!")(_ > 0)
Finally, we can call getOrElse or fold to extract values from the Valid
and Invalid cases:
"fail".invalid[Int].getOrElse(0)
// res22: Int = 0
Our goal is to implement code that parses the incoming data enforcing
the following rules:
If all the rules pass, our parser we should return a User. If any rules fail,
we should return a List of the error messages.
To implement this complete example well need to combine rules in se-
quence and in parallel Well use Either to combine computa ons in
sequence using fail-fast seman cs, and Validated to combine them in
parallel using accumula ng seman cs.
Lets start with some sequen al combina on. Well dene two methods
to read the "name" and "age" elds:
6.5. APPLY AND APPLICATIVE 165
Cartesians arent men oned frequently in the wider func onal pro-
gramming literature. They provide a subset of the func onality of a
166 CHAPTER 6. CARTESIANS AND APPLICATIVES
Cats models Applicatives using two type classes. The rst, Apply,
extends Cartesian and Functor and adds an ap method that applies
a parameter to a func on within a context. The second, Applicative
extends Apply, adds the pure method introduced in Chapter 4. Heres
a simplied deni on in code:
Applicative also introduces the pure method. This is the same pure
we saw in Monad. It constructs a new applica ve instance from an
unwrapped value. In this sense, Applicative is related to Apply as
Monoid is related to Semigroup.
6.5. APPLY AND APPLICATIVE 167
Each type class in the hierarchy represents a par cular set of sequenc-
ing seman cs. It introduces its characteris c methods, and denes all
of the func onality from its supertypes in terms of them. Every monad
is an applica ve, every applica ve a cartesian, and so on.
Because of the lawful nature of the rela onships between the type
classes, the inheritance rela onships are constant across all instances
of a type class. Apply denes product in terms of ap and map; Monad
denes product, ap, and map, in terms of pure and flatMap.
What can we say about these two data types without knowing more
about their implementa on?
This demonstrates the classic trade-o of power (in the mathema cal
sense) versus constraint. The more constraints we place on a data type,
the more guarantees we have about its behaviour, but the fewer be-
haviours we can model.
6.6 Summary
While monads and functors are the most widely used sequencing data
types weve covered in this book, cartesians and applica ves are the
most general. These type classes provide a generic mechanism to com-
bine values and apply func ons within a context, from which we can
fashion monads and a variety of other combinators.
Cartesians and applica ves are most commonly used as a means of com-
bining independent values such as the results of valida on rules. Cats
provides the Validated type for this specic purpose, along with carte-
sian builder syntax as a convenient way to express the combina on of
rules.
In this chapter well look at two type classes that capture itera on over
collec ons:
Well start by looking at Foldable, and then examine cases where fold-
ing becomes complex and Traverse becomes convenient.
7.1 Foldable
The Foldable type class captures the foldLeft and foldRight meth-
ods were used to in sequences like Lists, Vectors, and Streams. Us-
ing Foldable, we can write generic folds that work with a variety of
sequence types. We can also invent new sequences and plug them
into our code. Foldable gives us great use cases for Monoids and the
Eval monad.
171
172 CHAPTER 7. FOLDABLE AND TRAVERSE
show(Nil)
// res0: String = nil
show(List(1, 2, 3))
// res1: String = 3 then 2 then 1 then nil
1 1
2 2
3
3
0 + 1
3 + 0
1 + 2 2 + 3
3 + 3 1 + 5
6 6
List(1, 2, 3).foldLeft(0)(_ + _)
// res2: Int = 6
List(1, 2, 3).foldRight(0)(_ + _)
// res3: Int = 6
List(1, 2, 3).foldLeft(0)(_ - _)
// res4: Int = -6
List(1, 2, 3).foldRight(0)(_ - _)
// res5: Int = 2
Try using foldLeft and foldRight with an empty list as the accumu-
lator and :: as the binary operator. What results do you get in each
case?
foldLeft and foldRight are very general methods. We can use them
to implement many of the other high-level sequence opera ons we
know. Prove this to yourself by implemen ng subs tutes for List's
map, flatMap, filter, and sum methods in terms of foldRight.
import cats.Foldable
import cats.instances.list._
Foldable[List].foldLeft(ints, 0)(_ + _)
// res1: Int = 6
Other sequences like Vector and Stream work in the same way. Here
is an example using Option, which is treated like a sequence of 0 or 1
elements:
import cats.instances.option._
Foldable[Option].foldLeft(maybeInt, 10)(_ * _)
// res3: Int = 1230
Finally, here is an example for Map. The Foldable instance folds over
the values in the map (as opposed to its keys). Map has two type param-
eters so we have to x the key type to summon the Foldable:
import cats.instances.map._
Using Eval means folding is always stack safe, even when the collec-
ons default deni on of foldRight is not. For example, the default
implementa on of foldRight for Stream is not stack safe. The longer
the stream, the larger the stack requirements for the fold. A suciently
large stream will trigger a StackOverflowException:
import cats.Eval
import cats.Foldable
bigData.foldRight(0)(_ + _)
176 CHAPTER 7. FOLDABLE AND TRAVERSE
// java.lang.StackOverflowError ...
Using Foldable forces us to use stack safe opera ons, which xes the
overow excep on:
import cats.instances.stream._
eval.value
// res10: Int = 705082704
Stack safety isnt typically an issue when using the standard li-
brary. The most commonly used collec on types, such as List
and Vector, provide stack safe implementa ons of foldRight:
(1 to 100000).toList.foldRight(0)(_ + _)
// res11: Int = 705082704
(1 to 100000).toVector.foldRight(0)(_ + _)
// res12: Int = 705082704
Foldable[Option].nonEmpty(Option(42))
// res13: Boolean = true
Foldable[List].find(List(1, 2, 3))(_ % 2 == 0)
// res14: Option[Int] = Some(2)
combineAll (and its alias fold) combines all elements in the se-
quence using their Monoid;
Foldable[List].combineAll(List(1, 2, 3))
// res15: Int = 6
Alterna vely, we can use foldMap to convert each Int to a String and
concatenate them:
Foldable[List].foldMap(List(1, 2, 3))(_.toString)
// res16: String = 123
import cats.syntax.foldable._
List(1, 2, 3).combineAll
// res19: Int = 6
List(1, 2, 3).foldMap(_.toString)
// res20: String = 123
List(1, 2, 3).foldLeft(0)(_ + _)
// res21: Int = 6
import scala.language.higherKinds
7.2 Traverse
foldLeft and foldRight are exible itera on methods but they re-
quire us to do a lot of work to dene accumulators and combinator
func ons. The Traverse type class is a higher level tool that leverages
Applicatives to provide a more convenient, more lawful, pa ern for
itera on.
import scala.concurrent._
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
Now, suppose we want to poll all of the hosts and collect all of their
up mes. We cant simply map over hostnames because the resulta
List[Future[Int]]would contain more than one Future. We need
to reduce the results to a single Future to get something we can block
on. Lets start by doing this manually using a fold:
Await.result(allUptimes, 1.second)
// res2: List[Int] = List(1020, 960, 840)
Intui vely, we iterate over hostnames, call func for each item, and com-
bine the results into a list. This sounds simple, but the code is fairly
unwieldy because of the need to create and combine Futures at every
itera on. We can improve on things greatly using Future.traverse,
which is tailor made for this pa ern:
7.2. TRAVERSE 181
Await.result(allUptimes, 1.second)
// res3: List[Int] = List(1020, 960, 840)
object Future {
def traverse[A, B](values: List[A])
(func: A => Future[B]): Future[List[B]] =
values.foldLeft(Future(List.empty[A])) { (accum, host) =>
val item = func(host)
for {
accum <- accum
item <- item
} yield accum :+ item
}
}
This is essen ally the same as our example code above. Future.traverse
is abstrac ng away the pain of folding and dening accumulators and
combina on func ons. It gives us a clean high-level interface to do
what we want:
object Future {
def sequence[B](futures: List[Future[B]]): Future[List[B]] =
traverse(futures)(identity)
// etc...
}
Future(List.empty[Int])
is equivalent to Applicative.pure:
7.2. TRAVERSE 183
import cats.Applicative
import cats.instances.future._
import cats.syntax.applicative._
List.empty[Int].pure[Future]
Await.result(
listTraverse(hostnames)(getUptime),
1.second
)
// res11: List[Int] = List(1020, 960, 840)
import cats.instances.vector._
import cats.instances.option._
What is the return type of this method? What does it produce for the
following inputs?
process(List(2, 4, 6))
process(List(1, 2, 3))
import cats.data.Validated
import cats.instances.list._ // Applicative[ErrorsOr] needs a
Monoid[List]
process(List(2, 4, 6))
process(List(1, 2, 3))
trait Traverse[F[_]] {
def traverse[G[_] : Applicative, A, B](inputs: F[A])(func: A
=> G[B]): G[F[B]]
import cats.Traverse
import cats.instances.future._
import cats.instances.list._
Await.result(
Traverse[List].traverse(hostnames)(getUptime),
1.second
7.2. TRAVERSE 187
)
// res0: List[Int] = List(1020, 960, 840)
Await.result(
Traverse[List].sequence(numbers),
1.second
)
// res1: List[Int] = List(1, 2, 3)
import cats.syntax.traverse._
Await.result(hostnames.traverse(getUptime), 1.second)
// res2: List[Int] = List(1020, 960, 840)
Await.result(numbers.sequence, 1.second)
// res3: List[Int] = List(1, 2, 3)
As you can see, this is much more compact and readable than the
foldLeft code we started with earlier this chapter!
import cats.instances.list._
import cats.syntax.traverse._
eithers.sequence
// <console>:20: error: Cannot prove that Either[String,String]
<:< G[A].
// eithers.sequence
// ^
The reason for this failure is that the compiler cant nd an implicit
Applicative. This isnt a problem in our codewe have the correct
syntax and instances in scopeits simply a weakness of Scalas type in-
ference that has only recently been xed (more on the x in a moment).
To understand whats going on, lets look again at the deni on of
sequence:
trait Traverse[F[_]]
def sequence[G[_]: Applicative, B]: G[F[B]] =
// etc...
}
import cats.instances.either._
eithers.sequenceU
// res2: scala.util.Either[String,List[String]] = Right(List(Wow
!, Such cool!))
Fixes to SI-2712
7.3 Summary
a host of situa onally useful addi ons. That said, Foldable doesnt
introduce much that we didnt already know.
The real power comes from Traverse, which abstracts and generalises
the traverse and sequence methods we know from Future. Using
these methods we can turn an F[G[A]] into a G[F[A]] for any F with
an instance of Traverse and any G with an instance of Applicative.
In terms of the reduc on we get in lines of code, Traverse is one of
the most powerful pa erns in this book. We can reduce folds of many
lines down to a single foo.traverse.
Finally we looked at the Unapply type class, which works around re-
stric ons in the compiler and allows us to use methods like traverse
with types that have mul ple type parameters. Fixes in recent releases
of Scala make Unapply less important than it once was, but will s ll be
a necessity in many Scala versions to come.
and with that, weve nished all of the theory in this book. Theres
plenty more to come, though, as we put everything weve learned into
prac ce in a series of in-depth case studies in part 2!
Part II
Case Studies
191
Chapter 8
Well start with a simple case study: how to simplify unit tests for asyn-
chronous code by making them synchronous.
import scala.concurrent.Future
trait UptimeClient {
def getUptime(hostname: String): Future[Int]
}
193
194 CHAPTER 8. CASE STUDY: TESTING ASYNCHRONOUS CODE
import cats.instances.future._
import cats.instances.list._
import cats.syntax.traverse._
import scala.concurrent.ExecutionContext.Implicits.global
def testTotalUptime() = {
val hosts = Map("host1" -> 10, "host2" -> 6)
val client = new TestUptimeClient(hosts)
val service = new UptimeService(client)
val actual = service.getTotalUptime(hosts.keys.toList)
val expected = hosts.values.sum
assert(actual == expected)
}
// <console>:31: warning: scala.concurrent.Future[Int] and Int
are unrelated: they will most likely never compare equal
// assert(actual == expected)
// ^
// error: No warnings can be incurred under -Xfatal-warnings.
8.1. ABSTRACTING OVER TYPE CONSTRUCTORS 195
The ques on is: what result type should we give to the abstract method
in UptimeClient? We need to abstract over Future[Int] and Int:
trait UptimeClient {
def getUptime(hostname: String): ???
}
At rst this may seem dicult. We want to retain the Int part from
each type but throw away the Future part in the test code. Fortu-
nately, Cats provides a solu on in terms of the iden ty type, Id, that
Technically this is a warning not an error. It has been promoted to an error in our
case because were using the -Xfatal-warnings ag on scalac.
196 CHAPTER 8. CASE STUDY: TESTING ASYNCHRONOUS CODE
package cats
type Id[A] = A
write out the method header for getUptime in each case to ver-
ify that it compiles.
def testTotalUptime() = {
val hosts = Map("host1" -> 10, "host2" -> 6)
val client = new TestUptimeClient(hosts)
val service = new UptimeService(client)
val actual = service.getTotalUptime(hosts.keys.toList)
val expected = hosts.values.sum
assert(actual == expected)
198 CHAPTER 8. CASE STUDY: TESTING ASYNCHRONOUS CODE
testTotalUptime()
8.3 Conclusions
This case study provides a nice introduc on to how Cats can help
us abstract over dierent computa onal scenarios. We used the
Applicative type class to abstract over asynchronous and syn-
chronous code. Leaning on a func onal abstrac on allows us to
specify the sequence of computa ons we want to perform without
worrying about the details of the implementa on.
Lets move on now to a more complex case study where type classes
will help us produce something more interes ng: a map-reduce-style
framework for parallel processing.
Chapter 9
If you have used Hadoop or otherwise worked in big data you will
have heard of MapReduce, which is a programming model for doing
parallel data processing across clusters tens or hundreds of machines
(aka nodes). As the name suggests, the model is built around a map
phase, which is the same map func on we know from Scala and the
Functor type class, and a reduce phase, which we usually call fold in
Scala.
199
200 CHAPTER 9. CASE STUDY: PYGMY HADOOP
map
foldLeft ,
In summary, our parallel fold will yield the correct results if:
What does this pa ern sound like? Thats right, weve come full cir-
cle back to Monoid, the rst type class we discussed in this book. We
are not the rst to recognise the importance of monoids. The monoid
design pa ern for map-reduce jobs is at the core of recent big data
systems such as Twi ers Summingbird.
Start by wri ng out the signature of foldMap. It should accept the fol-
lowing parameters:
202 CHAPTER 9. CASE STUDY: PYGMY HADOOP
2. Map step
3. Fold/reduce step
4. Final result
import cats.instances.int._
6. Final result
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
or an instance of Traverse:
import cats.instances.future._ // Applicative for Future
import cats.instances.list._ // Traverse for List
import cats.syntax.traverse._ // foo.sequence syntax
There are also Monad and Monoid implementa ons for Future available
from cats.instances.future:
9.3. PARALLELISING FOLDMAP 207
import cats.Monad
import cats.instances.future._
Monad[Future].pure(42)
import cats.Monoid
import cats.instances.int._
Monoid[Future[Int]].combine(Future(1), Future(2))
Now weve refreshed our memory of Futures, lets look at how we can
divide work into batches. We can query the number of available CPUs
on our machine using an API call from the Java standard library:
Runtime.getRuntime.availableProcessors
// res15: Int = 8
(1 to 10).toList.grouped(3).toList
// res16: List[List[Int]] = List(List(1, 2, 3), List(4, 5, 6),
List(7, 8, 9), List(10))
Use the techniques described above to split the work into batches, one
batch per CPU. Process each batch in a parallel thread. Refer back to
Figure 9.4 if you need to review the overall algorithm.
For bonus points, process the batches for each CPU using your imple-
menta on of foldMap from above.
9.4 Summary
Await.result(future2, 1.second)
// res4: Int = 501500
In this case study we will build a library for valida on. What do we mean
by valida on? Almost all programs must check their input meets certain
criteria. Usernames must not be blank, email addresses must be valid,
and so on. This type of valida on o en occurs in web forms, but it could
be performed on congura on les, on web service responses, and any
other case where we have to deal with data that we cant guarantee
is correct. Authen ca on, for example, is just a specialised form of
valida on.
211
212 CHAPTER 10. CASE STUDY: DATA VALIDATION
These goals assume were checking a single piece of data. We will also
need to combine checks across mul ple pieces of data. For a login form,
for example, well need to combine the check results for the username
and the password. This will turn out to be quite a small component of
the library, so the majority of our me will focus on checking a single
data item.
10.1. SKETCHING THE LIBRARY STRUCTURE 213
F[A]
A => F[A]
Combine checks
|@|
Not really. With a cartesian, both checks are applied to the same value
and result in a tuple with the value repeated. What we want feels
more like a monoid as shown in Figure 10.4. We can dene a sensi-
ble iden tya check that always passesand two binary combina on
operatorsand and or:
|+|
Well probably be using and and or about equally o en with our valida-
on library and it will be annoying to con nuously switch between two
monoids for combining rules. We consequently wont actually use the
monoid API: well use two separate methods, and and or, instead.
Monoids also feel like a good mechanism for accumula ng error mes-
sages. If we store messages as a List or NonEmptyList, we can even
use a pre-exis ng monoid from inside Cats.
map
flatMap
Weve now broken down our library into familiar abstrac ons and are
in a good posi on to begin development.
Our design revolves around a Check, which we said was a func on from
a value to a value in a context. As soon as you see this descrip on you
should think of something like
trait Check[E, A] {
def apply(value: A): Either[E, A]
// other methods...
}
If you think back to Essen al Scala, there are two func onal program-
ming pa erns that we should consider when dening a trait:
Type classes allow us to unify disparate data types with a common inter-
face. This doesnt seem like what were trying to do here. That leaves
us with an algebraic data type. Lets keep that thought in mind as we
explore the design a bit further.
Lets add some combinator methods to Check, star ng with and. This
method combines two checks into one, succeeding only if both checks
succeed. Think about implemen ng this method now. You should hit
some problems. Read on when you do!
trait Check[E, A] {
def and(that: Check[E, A]): Check[E, A] =
???
10.3. BASIC COMBINATORS 217
// other methods...
}
You should very quickly run into a problem: what do you do when both
checks fail? The correct thing to do is to return both errors, but we
dont currently have any way to combine Es. We need a type class that
abstracts over the concept of accumula ng errors as shown in Figure
10.6
E E => E
What type class do we know that looks like this? What method or op-
erator should we use to implement the opera on?
See the solu on
There is another seman c issue that will come up quite quickly: should
and short-circuit if the rst check fails. What do you think the most
useful behavior is?
See the solu on
Use this knowledge to implement and. Make sure you end up with the
behavior you expect!
See the solu on
Strictly speaking, Either[E, A] is the wrong abstrac on for the out-
put of our check. Why is this the case? What other data type could we
use instead? Switch your implementa on over to this new data type.
218 CHAPTER 10. CASE STUDY: DATA VALIDATION
Checks can now represent opera ons like parsing a String as an Int:
10.4. TRANSFORMING DATA 219
However, spli ng our input and output types raises another issue. Up
un l now we have operated under the assump on that a Check always
returns its input when succesful. We used this in and and or to ignore
the output of the le and right rules and simply return the original input
on success:
// etc...
}
10.4.1 Predicates
import cats.Semigroup
import cats.data.Validated
import cats.syntax.semigroup._ // |+| syntax
import cats.syntax.cartesian._ // |@| syntax
import cats.data.Validated._ // Valid and Invalid
10.4.2 Checks
How do we relate F in the igure to Check in our code? Check has three
type variables while F only has one.
To unify the types we need to x two of the type parameters. The
idioma c choices are the error type E and the input type A. This gives
222 CHAPTER 10. CASE STUDY: DATA VALIDATION
flatMap
flatMap
trait Check[E, A, B] {
def andThen[C](that: Check[E, B, C]): Check[E, A, C]
}
10.4.3 Recap
We now have two algebraic data types, Predicate and Check, and a
host of combinators with their associated case class implementa ons.
Check the following solu on for a complete deni on of each ADT.
There are a lot of ways this library could be cleaned up. However, lets
implement some examples to prove to ourselves that our library really
does work, and then well turn to improving it.
Implement checks for some of the examples given in the introduc on:
224 CHAPTER 10. CASE STUDY: DATA VALIDATION
10.5 Kleislis
flatMap flatMap
We can also write out this example using the monad API as follows:
Recall that Check is, in the abstract, allowing us to compose func ons
of type A => F[B]. We can write the above in terms of andThen as:
226 CHAPTER 10. CASE STUDY: DATA VALIDATION
The result is a (wrapped) func on aToC of type A => F[C] that we can
subsequently apply to a value of type A.
We have achieved the same thing as the example method without hav-
ing to reference an argument of type A. The andThen method on Check
is analogous to func on composi on, but is composing func on A =>
F[B] instead of A => B.
The abstract concept of composing func ons of type A => F[B] has a
name: a Kleisli.
import cats.data.Kleisli
import cats.instances.list._
We can combine the steps into a single pipeline that combines the un-
derlying Lists using flatMap:
The result is a func on that consumes a single Int and returns eight
outputs, each produced by a dierent combina on of transforma ons
from step1, step2, and step3:
pipeline.run(20)
// res2: List[Int] = List(42, 10, -42, -10, 38, 9, -38, -9)
10.6 Conclusions
This case study has been an exercise in removing rather than building
abstrac ons. We started with a fairly complex Check type. Once we re-
alised we were cona ng two concepts, we separated out Predicate
leaving us with something that could be implemented with Kleisli.
231
232CHAPTER 11. CASE STUDY: COMMUTATIVE REPLICATED DATA TYPES
tem that is consistent, meaning that all machines have the same view of
data. For example, if a user changes their password then all machines
that store a copy of that password must accept the change before we
consider the opera on to have completed successfully.
Consistent systems are simple to work with but they have their disad-
vantages. They tend to have high latency, as every change can result is
many messages being sent between machines. They can also can have
rela vely low up me. A network problem can cause some machines to
be unable to communicate with others. This is called a network par -
on. When there is a network par on a consistent system may refuse
further updates as allowing them could result in data becoming incon-
sistent between the par oned systems.
An alterna ve approach is an eventually consistent system. This means
that if all machines can communicate and there are no further updates
they will evenutally all have the same view of data. However, at any
par cular point in me machines are allowed to have diering views of
data.
Latency can be lower because eventually consistent systems require
less communica on between machines. A par oned machine can s ll
accept updates and reconcile its changes when the network is xed, so
systems can also can have be er up me. How exactly are we to do this
reconcilia on, though? CRDTs are one approach to the problem.
Lets look at one par cular CRDT implementa on. Then well a empt
to generalise proper es to see if we can nd a general pa ern.
The data structure we will look at is called a GCounter. It is a distributed
increment-only counter. It can be used, for example, for coun ng the
number of visitors to a site where requests are served by many web
servers.
11.2. THE GCOUNTER 233
To see why a straigh orward counter wont work, imagine we have two
servers storing a count of visitors. Lets call the machines A and B. Each
machine is storing just an integer counter and the counters all start at
zero.
A: 0
B: 0
A: 3
B: 2
Now the machines want to merge their counters so they each have an
up-to-date view of the total number of visitors. At this point we know
the machines should add together their counters, because we know the
history of their interac ons. However, there is nothing in the data the
machines store that records this. Nonetheless, lets use addi on as our
strategy for merging counters and see what happens.
A: 5
B: 5
A: 6
B: 4
A: 10
B: 10
This is clearly wrong! There have only been six visitors in total. Do we
need to store the complete history of interac ons to be able to compute
the correct value? It turns out we do not, so lets look at the GCounter
now to see how it solves this problem in an elegant way.
11.2.2 GCounters
The rst clever idea in the GCounter is to have each machine storing a
separate counter for every machine (including itself) that it knows about.
In the previous example we had two machines, A and B. In this situa on
both machines would store a counter for A and a counter for B.
Machine A Machine B
A: 0 A: 0
B: 0 B: 0
The rule with these counters is that a given machine is only allowed
to increment its own counter. If A serves 3 visitors and B serves two
visitors the counters will look like
Machine A Machine B
A: 3 A: 0
B: 0 B: 2
Now when two machines merge their counters the rule is to take the
largest value stored for a given machine. Given the state above, when
A and B merge counters the result will be
11.2. THE GCOUNTER 235
Machine A Machine B
A: 3 A: 3
B: 2 B: 2
as 3 is the largest value stored for the A counter, and 2 is the largest
value stored for the B counter. The combina on of only allowing ma-
chines to increment their counter and choosing the maximum value on
merging means we get the correct answer without storing the complete
history of interac ons.
Machine A Machine B
A: 3 A: 3
B: 2 B: 2
11.3 Generalisa on
The proper es merge relies on are a bit more interes ng. We rely on
commu vity to ensure that machine A merging with machine B yields
the same result as machine B merging with machine A. We need as-
socia vity to ensure we obtain the correct result when three or more
machines are merging data. We need an iden ty element to ini alise
empty counters. Finally, we need an addi onal property, called idem-
potency, to ensure that if two machines hold the same data in a per-
machine counter, merging data will not lead to an incorrect result. For-
mally, a binary opera on max is idempotent if a max a = a.
Commuta ve Idempotent
Method Iden ty Associa ve
increment Y N Y N
get Y Y Y N
merge Y Y Y Y
Since increment and get both use the same binary opera on (addi on)
its usual to require the same commuta ve monoid for both.
the empty set. Set union is idempotent, commuta ve, and associa ve
and therefore ts all our requirements to work with a GCounter. With
this simple subs tu on of Int for Set[A] we can create a GSet type.
11.3.1 Implementa on
import cats.Monoid
11.3.2 Exercises
A closely related library called Spire provides both these abstrac ons.
11.4. ABSTRACTING GCOUNTER TO A TYPE CLASS 239
When you implement this, look for opportuni es to use methods and
syntax on monoid to simplify your implementa on. This is a good ex-
ample of how type class abstrac ons work at mul ple levels of code.
Were using monoids to design a large componentour CRDTsbut
they are also useful in the small, making our code simpler.
Weve created a generic GCounter that works with any value that has
(commuta ve) Monoid and BoundedSemiLattice type class instances.
However were s ll ed to a par cular representa on of the map from
machine IDs to values. There is no need to have this restric on, and
indeed it can be useful to abstract away from it. There are many key-
value stores that might like to work with our GCounter, from a simple
Map to a rela onal database.
There are a number of ways we can implement this. Try your own im-
plementa on before reading on.
type class as taking a higher-kinded type with two type parameters, in-
tended to represent the key and value types of the map abstrac on.
import scala.language.higherKinds
import cats.Monoid
trait GCounter[F[_,_],K, V] {
def increment(f: F[K, V])(k: K, v: V)(implicit m: Monoid[V]):
F[K, V]
def total(f: F[K, V])(implicit m: Monoid[V]): V
def merge(f1: F[K, V], f2: F[K, V])(implicit b:
BoundedSemiLattice[V]): F[K, V]
}
We can easily dene some instances of this type class. Heres a com-
plete example, containing a type class instance for Map and a simple
11.4. ABSTRACTING GCOUNTER TO A TYPE CLASS 241
test.
import cats.syntax.semigroup._
import cats.syntax.foldable._
object GCounterExample {
trait BoundedSemiLattice[A] extends Monoid[A] {
def combine(a1: A, a2: A): A
def empty: A
}
object BoundedSemiLattice {
implicit object intBoundedSemiLatticeInstance extends
BoundedSemiLattice[Int] {
def combine(a1: Int, a2: Int): Int =
a1 max a2
trait GCounter[F[_,_],K, V] {
def increment(f: F[K, V])(k: K, v: V)(implicit m: Monoid[V])
: F[K, V]
def total(f: F[K, V])(implicit m: Monoid[V]): V
def merge(f1: F[K, V], f2: F[K, V])(implicit b:
BoundedSemiLattice[V]): F[K, V]
}
object GCounter {
implicit def mapGCounterInstance[K, V]: GCounter[Map, K, V]
=
242CHAPTER 11. CASE STUDY: COMMUTATIVE REPLICATED DATA TYPES
new GCounter[Map, K, V] {
import cats.instances.map._
import cats.instances.int._
trait KeyValueStore[F[_,_]] {
def +[K, V](f: F[K, V])(key: K, value: V): F[K, V]
def get[K, V](f: F[K, V])(key: K): Option[V]
11.4. ABSTRACTING GCOUNTER TO A TYPE CLASS 243
object KeyValueStore {
implicit class KeyValueStoreOps[F[_,_],K, V](f: F[K, V]) {
def +(key: K, value: V)(implicit kv: KeyValueStore[F]): F[K,
V] =
kv.+(f)(key, value)
import cats.Foldable
Heres the complete code, including an example. This code is quite long
but the majority of it is boilerplate. We could cut down on the boiler-
plate by using compiler plugins such as Simulacrum and Kind Projector.
object GCounterExample {
import cats.{Monoid, Foldable}
import cats.syntax.foldable._
import cats.syntax.semigroup._
import scala.language.higherKinds
trait GCounter[F[_,_],K, V] {
def increment(f: F[K, V])(key: K, value: V)(implicit m:
Monoid[V]): F[K, V]
def total(f: F[K, V])(implicit m: Monoid[V]): V
def merge(f1: F[K, V], f2: F[K, V])(implicit b:
BoundedSemiLattice[V]): F[K, V]
}
object GCounter {
def apply[F[_,_],K, V](implicit g: GCounter[F, K, V]) = g
BoundedSemiLattice[V]): F[K, V] =
g.merge(f, that)
}
trait KeyValueStore[F[_,_]] {
def +[K, V](f: F[K, V])(key: K, value: V): F[K, V]
def get[K, V](f: F[K, V])(key: K): Option[V]
object KeyValueStore {
implicit class KeyValueStoreOps[F[_,_],K, V](f: F[K, V]) {
def +(key: K, value: V)(implicit kv: KeyValueStore[F]): F[
K, V] =
kv.+(f)(key, value)
kv.get(f)(key)
object Example {
import cats.instances.map._
import cats.instances.int._
import KeyValueStore._
import GCounter._
crdt1.increment("a", 20).merge(crdt2).total
}
}
248CHAPTER 11. CASE STUDY: COMMUTATIVE REPLICATED DATA TYPES
11.5 Summary
In this case study weve seen how we can use type classes to model a
simple CRDT, the GCounter, in Scala. Our implementa on gives us a
lot of exibility and code reuse. We are not ed to the data type we
count, nor to the data type that maps machine IDs to counters.
The focus in this case study has been on using the tools that Scala pro-
vides, and not on exploring CRDTs. There are many other CRDTs, some
of which operate in a similar manner to the GCounter, and some of
which have very dierent implementa ons. A fairly recent survey gives
a good overview of many of the basic CRDTs. However this is an ac ve
area of research and we encourage you to read the recent publica ons
in the eld if CRDTs and eventually consistency interest you.
Part III
249
Appendix A
These steps dene the three main components of our type class. First
we dene Printablethe type class itself:
trait Printable[A] {
def format(value: A): String
}
object PrintableInstances {
implicit val stringPrintable = new Printable[String] {
def format(input: String) = input
}
251
252 APPENDIX A. SOLUTIONS FOR: INTRODUCTION
}
}
object Printable {
def format[A](input: A)(implicit p: Printable[A]): String =
p.format(input)
This is a standard use of the type class pa ern. First we dene a set of
custom data types for our applica on:
Then we dene type class instances for the types we care about. These
either go into the companion object of Cat or a separate object to act
as a namespace:
import PrintableInstances._
}
}
Finally, we use the type class by bringing the relevant instances into
scope and using interface object/syntax. If we dened the instances in
companion objects Scala brings them into scope for us automa cally.
Otherwise we use an import to access them:
Printable.print(cat)
// Garfield is a 35 year-old ginger and black cat.
object PrintableSyntax {
implicit class PrintOps[A](value: A) {
def format(implicit p: Printable[A]): String =
p.format(value)
With PrintOps in scope, we can call the imaginary print and format
methods on any value for which Scala can locate an implicit instance of
Printable:
254 APPENDIX A. SOLUTIONS FOR: INTRODUCTION
import PrintableSyntax._
import java.util.Date
new Date().print
// <console>:34: error: could not find implicit value for
parameter p: Printable[java.util.Date]
// new Date().print
// ^
First lets import everything we need from Cats: the Show type class,
the instances for Int and String, and the interface syntax:
import cats.Show
import cats.instances.int._
import cats.instances.string._
import cats.syntax.show._
Finally, we use the Show interface syntax to print our instance of Cat:
println(Cat("Garfield", 35, "ginger and black").show)
// Garfield is a 35 year-old ginger and black cat.
First we need our Cats imports. In this exercise well be using the Eq
type class and the Eq interface syntax. Well bring instances of Eq into
scope as we need them below:
import cats.Eq
import cats.syntax.eq._
We bring the Eq instances for Int and String into scope for the imple-
menta on of Eq[Cat]:
import cats.instances.option._
There are four monoids for Boolean! First, we have and with operator
&& and iden ty true:
257
258 APPENDIX B. SOLUTIONS FOR: MONOIDS AND SEMIGROUPS
Finally, we have exclusive nor (the nega on of exclusive or) with iden ty
true:
Showing that the iden ty law holds in each case is straigh orward. Sim-
ilarly associa vity of the combine opera on can be shown by enumer-
a ng the cases.
We can write the addi on as a simple foldLeft using 0 and the + op-
erator:
260 APPENDIX B. SOLUTIONS FOR: MONOIDS AND SEMIGROUPS
We can alterna vely write the fold using Monoids, although theres not
a compelling use case for this yet:
import cats.Monoid
import cats.syntax.semigroup._
Now there is a use case for Monoids. We need a single method that
adds Ints and instances of Option[Int]. We can write this as a
generic method that accepts an implicit Monoid as a parameter:
import cats.Monoid
import cats.syntax.semigroup._
We can op onally use Scalas context bound syntax to write the same
code in a friendlier way:
We can use this code to add values of type Int and Option[Int] as
requested:
B.5. ADDING ALL THE THINGS PART 3 261
import cats.instances.int._
add(List(1, 2, 3))
// res9: Int = 6
import cats.instances.option._
import cats.Functor
import cats.syntax.functor._
263
264 APPENDIX C. SOLUTIONS FOR: FUNCTORS
Branch(Leaf(10), Leaf(20)).map(_ * 2)
// <console>:38: error: value map is not a member of Branch[Int]
// Branch(Leaf(10), Leaf(20)).map(_ * 2)
// ^
Oops! This is the same invariance problem we saw with Monoids. The
compiler cant nd a Functor instance for Leaf. Lets add some smart
constructors to compensate:
leaf(100).map(_ * 2)
// res6: Tree[Int] = Leaf(200)
branch(leaf(10), leaf(20)).map(_ * 2)
// res7: Tree[Int] = Branch(Leaf(20),Leaf(40))
trait Printable[A] {
def format(value: A): String
self.format(func(value))
}
}
}
To make the instance generic across all types of Box, we base it on the
Printable for the type inside the Box:
self.decode(value).map(dec)
}
}
}
D.1 Ge ng Func-y
At rst glance this seems tricky, but if we follow the types well see
theres only one solu on. Lets start by wri ng the method header:
trait Monad[F[_]] {
def pure[A](value: A): F[A]
Now we look at the types. Weve been given a value of type F[A].
Given the tools available theres only one thing we can do: call flatMap:
trait Monad[F[_]] {
def pure[A](value: A): F[A]
267
268 APPENDIX D. SOLUTIONS FOR: MONADS
trait Monad[F[_]] {
def pure[A](value: A): F[A]
import cats.Id
pure(123)
// res14: cats.Id[Int] = 123
map(123)(_ * 2)
// res15: cats.Id[Int] = 246
The nal punch line is that, once we strip away the Id type constructors,
flatMap and map are actually iden cal:
flatMap(123)(_ * 2)
// res16: cats.Id[Int] = 246
This is an open ques on. Its also kind of a trick ques onthe answer
depends on the seman cs were looking for. Some points to ponder:
import cats.Eval
Well start by dening a type alias for Writer so we can use it with pure
syntax:
import cats.data.Writer
import cats.syntax.applicative._
42.pure[Logged]
272 APPENDIX D. SOLUTIONS FOR: MONADS
import cats.syntax.writer._
Vector("Message").tell
// res16: cats.data.Writer[scala.collection.immutable.Vector[
String],Unit] = WriterT((Vector(Message),()))
Finally, well import the Semigroup instance for Vector. We need this
to map and flatMap over Logged:
import cats.instances.vector._
41.pure[Logged].map(_ + 1)
// res17: cats.data.WriterT[cats.Id,Vector[String],Int] =
WriterT((Vector(),42))
Now, when we call factorial, we have to run the result to extract the
log and our factorial:
D.6. HACKING ON READERS 273
Our type alias xes the Db type but leaves the result type exible:
type DbReader[A] = Reader[Db, A]
def checkPassword(
username: String,
password: String
): DbReader[Boolean] =
Reader(db => db.passwords.get(username).contains(password))
def checkLogin(
userId: Int,
password: String
): DbReader[Boolean] =
for {
username <- findUsername(userId)
passwordOk <- username.map { username =>
checkPassword(username, password)
}.getOrElse {
false.pure[DbReader]
}
} yield passwordOk
Lets look at operand rst. All we have to do is push a number onto the
stack. We also return the operand as an intermediate result:
case _ =>
sys.error("Fail!")
276 APPENDIX D. SOLUTIONS FOR: MONADS
import cats.syntax.applicative._
// import cats.syntax.applicative._
The code for flatMap is simple. Its similar to the code for map. Again,
we recurse down the structure and use the results from func to build
a new Tree.
The code for tailRecM is less simple. In fact, its fairly complex! How-
ever, if we follow the types the solu on falls out. Note that we cant
make tailRecM tail recursive in this case because we have to recurse
twice when processing a Branch. We implement the tailRecM method,
and we dont use the tailrec annota on:
D.11. BRANCHING OUT FURTHER WITH MONADS 277
import cats.Monad
import cats.syntax.functor._
import cats.syntax.flatMap._
branch(leaf(100), leaf(200)).
flatMap(x => branch(leaf(x - 1), leaf(x + 1)))
// res4: Tree[Int] = Branch(Branch(Leaf(99),Leaf(101)),Branch(
Leaf(199),Leaf(201)))
for {
a <- branch(leaf(100), leaf(200))
b <- branch(leaf(a - 10), leaf(a + 10))
c <- branch(leaf(b - 1), leaf(b + 1))
} yield c
// res5: Tree[Int] = Branch(Branch(Branch(Leaf(89),Leaf(91)),
Branch(Leaf(109),Leaf(111))),Branch(Branch(Leaf(189),Leaf
(191)),Branch(Leaf(209),Leaf(211))))
The monad for Option provides fail-fast seman cs. The monad for
List provides concatena on seman cs. What are the seman cs of
flatMap for a binary tree? Every node in the tree has the poten al to be
replaced with a whole subtree, producing a kind of growing or feath-
ering behaviour, reminiscent of list concatena on along two axes.
This is a rela vely simple combina on. We want Future on the outside
and Either on the inside, so we build from the inside out using an
EitherT of Future:
import cats.data.EitherT
import scala.concurrent.Future
279
280 APPENDIX E. SOLUTIONS FOR: MONAD TRANSFORMERS
import cats.instances.future._
import cats.syntax.flatMap._
import scala.concurrent.ExecutionContext.Implicits.global
We request the power level from each ally and use map and flatMap to
combine the results:
def canSpecialMove(
ally1: String,
ally2: String
): Response[Boolean] =
for {
power1 <- getPowerLevel(ally1)
power2 <- getPowerLevel(ally2)
} yield (power1 + power2) > 15
We use the value method to unpack the monad stack and Await and
fold to unpack the Future and Either:
E.4. MONADS: TRANSFORM AND ROLL OUT PART 4 281
import scala.concurrent.Await
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
def tacticalReport(
ally1: String,
ally2: String
): String =
Await.result(
canSpecialMove(ally1, ally2).value,
1.second
) match {
case Left(msg) =>
s"Comms error: $msg"
case Right(true) =>
s"$ally1 and $ally2 are ready to roll out!"
case Right(false) =>
s"$ally1 and $ally2 need a recharge."
}
import cats.syntax.flatMap._
import cats.syntax.functor._
283
284 APPENDIX F. SOLUTIONS FOR: CARTESIANS AND APPLICATIVES
} yield (a, b)
The seman cs of flatMap are what give rise to the behaviour for List
and Either:
import cats.instances.list._
Even our results for Future are a trick of the light. flatMap provides
sequen al ordering, so product provides the same. The only reason we
get parallel execu on is because our cons tuent Futures start running
before we call product. This is equivalent to the classic create-then-
atmap pa ern:
val a = Future("Future 1")
val b = Future("Future 2")
for {
x <- a
y <- b
} yield (x, y)
Well be using Either and Validated so well start with some imports:
F.3. FORM VALIDATION PART 2 285
import cats.data.Validated
The getValue rule extracts a String from the form data. Well be using
it in sequence with rules for parsing Ints and checking values, so well
dene it to return an Either:
def getValue(name: String)(data: FormData): ErrorsOr[String] =
data.get(name).
toRight(List(s"$name field not specified"))
getName(Map())
// res26: ErrorsOr[String] = Left(List(name field not specified)
)
Note that our solu on accepts an extra parameter to name the eld
were parsing. This is useful for crea ng be er error messages, but its
ne if you leave it out in your code.
If we provide valid input, parseInt converts it to an Int:
parseInt("age")("11")
// res28: ErrorsOr[Int] = Right(11)
parseInt("age")("foo")
// res29: ErrorsOr[Int] = Left(List(age must be an integer))
nonBlank("name")("Dade Murphy")
// res31: ErrorsOr[String] = Right(Dade Murphy)
nonBlank("name")("")
// res32: ErrorsOr[String] = Left(List(name cannot be blank))
nonNegative("age")(11)
// res33: ErrorsOr[Int] = Right(11)
nonNegative("age")(-1)
// res34: ErrorsOr[Int] = Left(List(age must be non-negative))
The rules pick up all the error cases weve seen so far:
readName(Map("name" -> "Dade Murphy"))
// res36: ErrorsOr[String] = Right(Dade Murphy)
readName(Map())
288 APPENDIX F. SOLUTIONS FOR: CARTESIANS AND APPLICATIVES
readAge(Map())
// res41: ErrorsOr[Int] = Left(List(age field not specified))
import cats.syntax.cartesian._
The need to switch back and forth between Either and Validated
is annoying. The choice of whether to use Either or Validated as a
default is determined by context. In applica on code, we typically nd
areas that favour accumula ng seman cs and areas that favour fail-fast
seman cs. We pick the data type that best suits our need and switch
to the other as necessary in specic situa ons.
291
292 APPENDIX G. SOLUTIONS FOR: FOLDABLE AND TRAVERSE
List(1, 2, 3).foldRight(Nil)(_ :: _)
// <console>:13: error: type mismatch;
// found : List[Int]
// required: scala.collection.immutable.Nil.type
// List(1, 2, 3).foldRight(Nil)(_ :: _)
// ^
map(List(1, 2, 3))(_ * 2)
// res9: List[Int] = List(2, 4, 6)
filter(List(1, 2, 3))(_ % 2 == 1)
// res11: List[Int] = List(1, 3)
G.3. TRAVERSING WITH VECTORS 293
import scala.math.Numeric
sumWithNumeric(List(1, 2, 3))
// res13: Int = 6
import cats.instances.int._
sumWithMonoid(List(1, 2, 3))
// res16: Int = 6
With three items in the input list, we end up with combina ons of three
Ints: one from the rst item, one from the second, and one from the
third:
listSequence(List(Vector(1, 2), Vector(3, 4), Vector(5, 6)))
// res16: scala.collection.immutable.Vector[List[Int]] = Vector(
List(1, 3, 5), List(1, 3, 6), List(1, 4, 5), List(1, 4, 6),
List(2, 3, 5), List(2, 3, 6), List(2, 4, 5), List(2, 4, 6))
process(List(2, 4, 6))
// res20: Option[List[Int]] = Some(List(2, 4, 6))
process(List(1, 2, 3))
// res21: Option[List[Int]] = None
process(List(2, 4, 6))
// res26: ErrorsOr[List[Int]] = Valid(List(2, 4, 6))
process(List(1, 2, 3))
// res27: ErrorsOr[List[Int]] = Invalid(List(1 is not even, 3 is
not even))
import scala.language.higherKinds
import cats.Id
trait UptimeClient[F[_]] {
def getUptime(hostname: String): F[Int]
}
Note that, because Id[A] is just a simple alias for A, we dont need
297
298APPENDIX H. SOLUTIONS FOR: CASE STUDY: TESTING ASYNCHRONOUS CODE
???
// hostnames.traverse(client.getUptime).map(_.sum)
}
import cats.Applicative
import cats.syntax.functor._
301
302 APPENDIX I. SOLUTIONS FOR: CASE STUDY: PYGMY HADOOP
import cats.Monoid
import cats.instances.int._
import cats.instances.string._
import cats.syntax.semigroup._
Here is an annotated solu on that splits out each map and fold into a
separate line of code:
import scala.concurrent.duration.Duration
Await.result(parallelFoldMap((1 to 1000000).toVector)(identity),
1.second)
// res18: Int = 1784293664
We can re-use our deni on of foldMap for a more concise solu on.
Note that the local maps and reduces in steps 3 and 4 of Figure 9.4 are
actually equivalent to a single call to foldMap, shortening the en re
algorithm as follows:
Await.result(parallelFoldMap((1 to 1000000).toVector)(identity),
304 APPENDIX I. SOLUTIONS FOR: CASE STUDY: PYGMY HADOOP
1.second)
// res19: Int = 1784293664
import scala.concurrent._
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
values
.grouped(groupSize)
.toVector
I.4. PARALLELFOLDMAP WITH MORE CATS 305
Await.result(future, 1.second)
// res3: Int = 500500000
Note we dont need a full Monoid because we dont need the iden ty
307
308 APPENDIX J. SOLUTIONS FOR: CASE STUDY: DATA VALIDATION
We want to report all the errors we can, so we should prefer not short-
circui ng whenever possible.
In the case of the and method, the two checks were combining are in-
dependent of one another. We can always run both rules and combine
any errors we see.
Return to the exercise
import cats.Semigroup
import cats.syntax.either._ // asLeft and asRight syntax
import cats.syntax.semigroup._ // |+| syntax
CheckF { a =>
(this(a), that(a)) match {
case (Left(e1), Left(e2)) => (e1 |+| e2).asLeft
case (Left(e), Right(a)) => e.asLeft
case (Right(a), Left(e)) => e.asLeft
case (Right(a1), Right(a2)) => a.asRight
}
}
}
Lets test the behavior we get. First well setup some checks:
check(5)
// res5: Either[List[String],Int] = Left(List(Must be < -2))
check(0)
310 APPENDIX J. SOLUTIONS FOR: CASE STUDY: DATA VALIDATION
Because of its exibility, we will use the ADT implementa on for the
rest of this case study.
Return to the exercise
The implementa on of apply for And is using the pa ern for applica ve
functors. Either has an Applicative instance, but it doesnt have the
seman cs we want/ It fails fast instead of accumula ng errors.
If we want to accumulate errors Validated is a more appropriate ab-
strac on. As a bonus, we get more code reuse because we can lean on
the applica ve instance of Validated in the implementa on of apply.
Heres the complete implementa on:
import cats.Semigroup
import cats.data.Validated
import cats.syntax.semigroup._ // |+| syntax
import cats.syntax.cartesian._ // |@| syntax
this match {
case Pure(func) =>
func(a)
This reuses the same technique for and. We have to do a bit more
work in the apply method. Note that its ok to short-circuit in this case
because the choice of rules is implicit in the seman cs of or.
import cats.Semigroup
import cats.data.Validated
import cats.syntax.semigroup._ // |+| syntax
import cats.syntax.cartesian._ // |@| syntax
import cats.data.Validated._ // Valid and Invalid
J.6 Checks
import cats.Semigroup
import cats.data.Validated
object Check {
def apply[E, A](pred: Predicate[E, A]): Check[E, A, A] =
Pure(pred)
}
import cats.Semigroup
import cats.data.Validated
import cats.syntax.either._
// other methods...
}
J.9 Recap
object Predicate {
final case class And[E, A](
left: Predicate[E, A],
right: Predicate[E, A]) extends Predicate[E, A]
=
Pure(a => if(func(a)) a.valid else error.invalid)
}
object Check {
final case class Map[E, A, B, C](
check: Check[E, A, B],
func: B => C) extends Check[E, A, C] {
at appropriate places felt a bit like guesswork ll we got the rule into
our heads that Predicate doesnt transform its input. With this rule
in mind things went fairly smoothly. In later sec ons well make some
changes that make the library easier to use.
import cats.data.{NonEmptyList, OneAnd, Validated}
import cats.instances.list._
import cats.syntax.cartesian._
import cats.syntax.validated._
def createUser(
username: String,
email: String): Validated[Errors, User] =
(checkUsername(username) |@| checkEmail(email)).map(User)
createUser("", "dave@underscore@io")
// res15: cats.data.Validated[wrapper.Errors,User] = Invalid(
NonEmptyList(Must be longer than 3 characters, Must contain
a single @ character))
One dis nct disadvantage of our example is that it doesnt tell us where
the errors came from. We can either achieve that through judicious
manipula on of error messages, or we can modify our library to track
error loca ons as well as messages. Tracking error loca ons is outside
the scope of this case study, so well leave this as an exercise to the
reader.
Return to the exercise
J.11. KLEISLIS 323
J.11 Kleislis
import cats.Semigroup
import cats.data.Validated
// other methods...
}
Here is the preamble we suggested in the main text of the case study:
324 APPENDIX J. SOLUTIONS FOR: CASE STUDY: DATA VALIDATION
Our username and email examples are slightly dierent in that we make
use of check() and checkPred() in dierent situa ons:
J.12. KLEISLIS PART 2 325
createUser("Noel", "[email protected]")
// res16: Either[Errors,User] = Right(User(Noel,noel@underscore.
io))
326 APPENDIX J. SOLUTIONS FOR: CASE STUDY: DATA VALIDATION
createUser("", "dave@underscore@io")
// res17: Either[Errors,User] = Left(NonEmptyList(Must be longer
than 3 characters))
Hopefully the descrip on above was clear enough that you can get to
an implementa on like the below.
327
328APPENDIX K. SOLUTIONS FOR: CASE STUDY: COMMUTATIVE REPLICATED DATA
object BoundedSemiLattice {
implicit object intBoundedSemiLatticeInstance extends
BoundedSemiLattice[Int] {
def combine(a1: Int, a2: Int): Int =
a1 max a2
Set.empty[A]
}
}