Functional Programming in Scala PDF
Functional Programming in Scala PDF
info
MEAP Edition
Manning Early Access Program
Functional Programming in Scala
version 10
www.it-ebooks.info
brief contents
PART 1: INTRODUCTION TO FUNCTIONAL PROGRAMMING
2. Getting Started
8. Property-based testing
9. Parser combinators
10. Monoids
11. Monads
www.it-ebooks.info
1
This is not a book about Scala. This book introduces the concepts and techniques
of functional programming (FP)—we use Scala as the vehicle, but the lessons
herein can be applied to programming in any language. Our goal is to give you the
foundations to begin writing substantive functional programs and to comfortably
absorb new FP concepts and techniques beyond those covered here. Throughout
the book we rely heavily on programming exercises, carefully chosen and
sequenced to guide you to discover FP for yourself. Expository text is often just
enough to lead you to the next exercise. Do these exercises and you will learn the
material. Read without doing and you will find yourself lost.
A word of caution: no matter how long you've been programming, learning FP
is challenging. Come prepared to be a beginner once again. FP proceeds from a
startling premise—that we construct programs using only pure functions, or
functions that avoid side effects like writing to a database or reading from a file. In
the first chapter, we will explain exactly what this means. From this single idea and
its logical consequences emerges a very different way of building programs, one
with its own body of techniques and concepts. We start by relearning how to write
the simplest of programs in a functional way. From this foundation we will build
the tower of techniques necessary for expressing functional programs of greater
complexity. Some of these techniques may feel alien or unnatural at first and the
exercises and questions can be difficult, even brain-bending at times. This is
normal. Don't be deterred. Keep a beginner's mind, try to suspend judgment, and if
www.it-ebooks.info
2
you must be skeptical, don't let this skepticism get in the way of learning. When
you start to feel more fluent at expressing functional programs, then take a step
back and evaluate what you think of the FP approach.
This book does not require any prior experience with Scala, but we won't spend
a lot of time and space discussing Scala's syntax and language features. Instead
we'll introduce them as we go, with a minimum of ceremony, mostly using short
examples, and mostly as a consequence of covering other material. These minimal
introductions to Scala should be enough to get you started with the exercises. If
you have further questions about the Scala language while working on the
exercises, you are expected to do some research and experimentation on your own
or follow some of our links to further reading.
www.it-ebooks.info
3
two purposes: to help you to understand the ideas being discussed and to guide you
to discover for yourself new ideas that are relevant. Therefore we strongly suggest
that you download the exercise source code and do the exercises as you go through
each chapter. Exercises, hints and answers are all available at
https://github.com/pchiusano/fpinscala. We also encourage you to visit the
scala-functional Google group and the #fp-in-scala IRC channel on
irc.freenode.net for questions and discussion.
Exercises are marked for both their difficulty and to indicate whether they are
critical or noncritical. We will mark exercises that we think are hard or that we
consider to be critical to understanding the material. The hard designation is our
effort to give you some idea of what to expect—it is only our guess and you may
find some unmarked questions difficult and some questions marked hard to be
quite easy. The critical designation is applied to exercises that address concepts
that we will be building on and are therefore important to understand fully.
Noncritical exercises are still informative but can be skipped without impeding
your ability to follow further material.
Examples are given throughout the book and they are meant to be tried rather
than just read. Before you begin, you should have the Scala interpreter (REPL)
running and ready. We encourage you to experiment on your own with variations
of what you see in the examples. A good way to understand something is to change
it slightly and see how the change affects the outcome.
Sometimes we will show a REPL session to demonstrate the result of running
some code. This will be marked by lines beginning with the scala> prompt of
the REPL. Code that follows this prompt is to be typed or pasted into the
interpreter, and the line just below will show the interpreter's response, like this:
SIDEBAR Sidebars
Occasionally throughout the book we will want to highlight the precise
definition of a concept in a sidebar like this one. This lets us give you a
complete and concise definition without breaking the flow of the main
text with overly formal language, and also makes it easy to refer back to
when needed.
There are chapter notes (which includes references to external resources) and
www.it-ebooks.info
4
several appendix chapters after Part 4. Throughout the book we provide references
to this supplementary material, which you can explore on your own if that interests
you.
Have fun and good luck.
www.it-ebooks.info
5
Reassigning a variable
Modifying a data structure in place
Setting a field on an object
Throwing an exception or halting with an error
Printing to the console or reading user input
Reading from or writing to a file
Drawing on the screen
www.it-ebooks.info
6
www.it-ebooks.info
7
functions. For the purposes of our discussion, consider an expression to be any part
of a program that can be evaluated to a result, i.e. anything that you could type into
the Scala interpreter and get an answer. For example, 2 + 3 is an expression that
applies the pure function + to the values 2 and 3 (which are also expressions). This
has no side effect. The evaluation of this expression results in the same value 5
every time. In fact, if you saw 2 + 3 in a program you could simply replace it
with the value 5 and it would not change a thing about your program.
This is all it means for an expression to be referentially transparent—in any
program, the expression can be replaced by its result without changing the meaning
of the program. And we say that a function is pure if its body is RT, assuming RT
inputs.
www.it-ebooks.info
8
Footnote 2mIn Java and in Scala, strings are immutable. If you wish to "modify" a string, you must create a
copy of it.
This transformation does not affect the outcome. The values of r1 and r2 are
the same as before, so x was referentially transparent. What's more, r1 and r2 are
referentially transparent as well, so if they appeared in some other part of a larger
program, they could in turn be replaced with their values throughout and it would
have no effect on the program.
Now let's look at a function that is not referentially transparent. Consider the
append function on the scala.collection.mutable.StringBuilder
class. This function operates on the StringBuilder in place. The previous state
of the StringBuilder is destroyed after a call to append. Let's try this out:
www.it-ebooks.info
9
So far so good. Let's now see how this side effect breaks RT. Suppose we
substitute the call to append like we did earlier, replacing all occurrences of y
with the expression referenced by y:
www.it-ebooks.info
10
"how to obtain the input"; it is a black box. Input is obtained in exactly one way:
via the argument(s) to the function. And the output is simply computed and
returned. By keeping each of these concerns separate, the logic of the computation
is more reusable; we may reuse the logic wherever we want without worrying
about whether the side effect being done with the result or the side effect being
done to request the input is appropriate in all contexts. We also do not need to
mentally track all the state changes that may occur before or after our function's
execution to understand what our function will do; we simply look at the function's
definition and substitute the arguments into its body.
Let's look at a case where factoring code into pure functions helps with reuse.
This is a simple and contrived example, intended only to be illustrative. Suppose
we are writing a computer game and are required to do the following:
If player 1's score property is greater than player 2's, notify the user that player
1 has won, otherwise notify the user that player 2 has won.
We may be tempted to write something like this:
Declares a data type Player with two properties: name, which is a string, and score,
an integer.
Prints the name of the winner to the console.
Takes two Players, compares their scores and declares the winner.
This declares a simple data type Player with two properties, name, which is
a character string, and score which is an integer. The method declareWinner
takes two Players, compares their scores and declares the player with the higher
score the winner (unfairly favoring the second player, granted). The
printWinner method prints the name of the winner to the console. The result
type of these methods is Unit indicating that they do not return a meaningful
result but have a side effect instead.
Let's test this in the REPL:
www.it-ebooks.info
11
While this code closely matches the earlier problem statement, it also
intertwines the branching logic with that of displaying the result, which makes the
reuse of the branching logic difficult. Consider trying to reuse the
declareWinner method to compute and display the sole winner among n
players instead of just two. In this case, the comparison logic is simple enough that
we could just inline it, but then we are duplicating logic—what happens when
playtesting reveals that our game unfairly favors one player, and we have to change
the logic for determining the winner? We would have to change it in two places.
And what if we want to use that same logic to sort a historical collection of past
players to display a high score list?
Suppose we refactor the code as follows:
A pure function that takes two players and returns the higher-scoring one.
This version separates the logic of computing the winner from the displaying of
the result. Computing the winner in winner is referentially transparent and the
impure part—displaying the result—is kept separate in printWinner. We can
now reuse the logic of winner to compute the winner among a list of players:
val p = players.reduceLeft(winner)
printWinner(p)
www.it-ebooks.info
12
Reduces the list to just the player with the highest score.
Prints the name of the winner to the console.
In this example, reduceLeft is a function on the List data type from the
standard Scala library. The expression will compare all the players in the list and
return the one with the highest score. Note that we are actually passing our
winner function to reduceLeft as if it were a regular value. We will have a lot
more to say about passing functions to functions, but for now just observe that
because winner is a pure function, we are able to reuse it and combine it with
other functions in ways that we didn't necessarily anticipate. In particular, this
usage of winner would not have been possible when the side effect of displaying
the result was interleaved with the logic for computing the winner.
This was just a simple example, meant to be illustrative, and the sort of
factoring we did here is something you've perhaps done many times before. It's
been said that functional programming, at least in small examples, is just normal
separation of concerns and "good software engineering".
We will be taking the idea of FP to its logical endpoint in this book, and
applying it in situations where is applicability is less obvious. As we'll learn, any
function with side effects can be split into a pure function at the "core" and
possibly a pair of functions with side effects; one on the input side, and one on the
output side. This is what we did when we separated the declaration of the winner
from our pure function winner. This transformation can be repeated to push side
effects to the "outer layers" of the program. Functional programmers often speak of
implementing programs with a pure core and a thin layer on the outside that
handles effects. We will return to this principle again and again throughout the
book.
1.5 Conclusion
In this chapter, we introduced functional programming and explained exactly what
FP is and why you might use it. In subsequent chapters, we cover some of the
fundamentals—how do we write loops in FP? Or implement data structures? How
do we deal with errors and exceptions? We need to learn how to do these things
and get comfortable with the low-level idioms of FP. We'll build on this
understanding when we explore functional design techniques in parts 2 and 3.
www.it-ebooks.info
13
Index Terms
composition
equals for equals
equational reasoning
expression substitution
modularity
program modularity
referential transparency
side effects
substitution
substitution model
www.it-ebooks.info
14
2.1 Introduction
2
Getting started
Now that we have committed to using only pure functions, a question naturally
emerges: how do we write even the simplest of programs? Most of us are used to
thinking of programs as sequences of instructions that are executed in order, where
each instruction has some kind of effect. In this chapter we will learn how to write
programs in the Scala language just by combining pure functions.
This chapter is mainly intended for those readers who are new to Scala, to
functional programming, or both. As with learning a foreign language, immersion
is a very effective method, so we will start by looking at a small but complete
Scala program. If you have no experience with Scala, you should not expect to
understand the code at first glance. Therefore we will break it down piece by piece
to look at what it does.
We will then look at working with higher-order functions. These are functions
that take other functions as arguments, and may themselves return functions as
their output. This can be brain-bending if you have a lot of experience
programming in a language without the ability to pass functions around like that.
Remember, it's not crucial that you internalize every single concept in this chapter,
or solve every exercise. In fact, you might find it easier to skip whole sections and
spiral back to them when you have more experience onto which to attach these
concepts.
// A comment!
www.it-ebooks.info
15
/* Another comment */
/** A documentation comment */
object MyModule {
def abs(n: Int): Int =
if (n < 0) -n
else n
www.it-ebooks.info
16
if (n < 0) -n
else n
The abs method takes a single argument n of type Int, and this is declared with n:
Int.
The definition is a single Scala expression that uses the built-in if syntax to negate
n if it's less than zero.
The format method is a standard library method defined on String. Here we are
calling it on the msg object, passing in the value of x along with the value of abs
applied to x. This results in a new string with the occurrences of %d in msg
replaced with the evaluated results of x and abs(x) respectively. Also see the
sidebar on string interpolation below.
This method is declared private, which means that it cannot be called from
any code outside of the MyModule object. This function takes an Int and returns
a String, but note that the return type is not declared. Scala is usually able to
infer the return types of methods, so they can be omitted, but it's generally
considered good style to explicitly declare the return types of methods that you
expect others to use. This method is private to our module, so we can omit the type
annotation.
The body of the method contains more than one statement, so we put them
inside curly braces. A pair of braces containing statements is called a block.
Statements are separated by new lines or by semicolons. In this case we are using a
new line to separate our statements.
The first statement in the block declares a String named msg using the val
keyword. A val is an immutable variable, so inside the body of the formatAs
method the name msg will always refer to the same String value. The Scala
compiler will complain if you try to reassign msg to a different value in the same
context.
Remember, a method simply returns the value of its right-hand side, which in
this case is a block. And the value of a multi-statement block inside curly braces is
simply the same as the value of its last statement. So the result of the formatAbs
www.it-ebooks.info
17
Finally, our main method is an "outer shell" that calls into our purely
functional core and performs the effect of printing the answer to the console:
The name main is special because when you run a program, Scala will look for
a method named main with a specific signature. It has to take an Array of
Strings as its argument, and its return type must be Unit. The args array will
contain the arguments that were given at the command line that ran the program.
The return type of Unit indicates that this method does not return a meaningful
value. There is only one value of type Unit and it has no inner structure. It's
written (), pronounced "unit" just like the type. Usually a return type of Unit is a
hint that the method has a side effect. But since the main method itself is called
once by the operating environment and never from anywhere in our program,
referential transparency is not violated.
www.it-ebooks.info
18
This will generate some files ending with the .class suffix. These files
contain compiled code that can be run with the Java virtual machine. The code can
be executed using the scala code runner:
Actually, it's not strictly necessary to compile the code first with scalac. A
simple program like the one we have written here can just be run using the Scala
interpreter by passing it to the scala code runner directly:
This can be handy when using Scala for scripting. The code runner will look for
any object within the file MyModule.scala that has a main method with the
appropriate signature, and will then call it.
Lastly, an alternative way is to start the Scala interpreter's interactive mode,
usually referred to as the read-evalulate-print-loop or REPL (pronounced "repple"
like "apple"), and load the file from there (your actual console output may differ
slightly):
> scala
Welcome to Scala.
Type in expressions to have them evaluated.
Type :help for more information.
scala> MyModule.main(Array())
The absolute value of -42 is 42.
www.it-ebooks.info
19
main takes an array as its argument and here we are simply passing it an empty
array.
It's possible to simply copy and paste the code into the REPL. It also has a paste
mode (accessed with the :paste command) specifically designed to paste code.
It's a good idea to get familiar with the REPL and its features.
scala> abs(-42)
res0: 42
www.it-ebooks.info
20
We can bring all of an object's (non-private) members into scope by using the
underscore syntax: import MyModule._
SIDEBAR Packages
In Scala, there is a language construct called a package, which is a
namespace without an object. The difference between a package and a
module is that a package cannot contain val or def members and
can't be passed around as if it were an object.
For example, we can declare a package at the start of our Scala source
file:
package mypackage
object MyModule {
...
}
First, let's write factorial, which also happens to be our first example of
www.it-ebooks.info
21
go(n, 1)
}
www.it-ebooks.info
22
We won't be talking much more about annotations in this book, but we'll
use @annotation.tailrec extensively.
EXERCISE 1 (optional): Write a function to get the nth Fibonacci number. The
first two Fibonacci numbers are 0 and 1, and the next number is always the sum of
the previous two. Your definition should use a local tail-recursive function.4
Footnote 4mNote that the nth Fibonacci number has a closed form solution. Using that would be cheating; the
point here is just to get some practice writing loops using tail-recursive functions.
Now that we have factorial, let's edit our program from before:
The two functions, formatAbs and formatFactorial, are almost
identical. If we like, we can generalize these to a single function, formatResult
, which accepts as an argument the function to apply to its argument:
www.it-ebooks.info
23
There are a few new thing here. First, our formatResult function takes
multiple arguments. To declare a function with multiple arguments, we just
separate each argument by a comma. Second, our formatResult function now
takes another function, which we call f (this is a common naming convention in
FP; see the sidebar below). A function that takes another function as an argument
is called a higher-order function (HOF). Like any other function parameter, we
give a type to f, the type Int => Int, which indicates that f expects an Int
and will also return an Int. (The type of a function expecting an Int and a
String and returning an Int would be written as (Int,String) => Int.)
Next, notice that we call the function f using the same syntax as when we
called abs(x) or factorial(n) directly. Lastly, notice that we can pass a
reference to abs and factorial to the formatResult function. Our function
abs accepts an Int and returns an Int, which matches the Int => Int
requirement on f in formatResult. And likewise, factorial accepts an
Int and returns an Int, which also matches the Int => Int requirement on f.
This example isn't terribly exciting, but the same principles apply in larger
examples, and we can use first-class functions to factor out duplication whenever
we see it. We'll see many more examples of this throughout this book.
www.it-ebooks.info
24
We could declare a value of this type like so val f = (x: Int) => x +
www.it-ebooks.info
25
1, but here we are not bothering to declare a local variable for the function, which
is quite common in FP. In this last form _ + 1, sometimes called underscore
syntax for a function literal, we are not even bothering to name the argument to the
function, using _ represent the sole argument. When using this notation, we can
only reference the function parameter once in the body of the function (if we
mention _ again, it refers to another argument to the function).7
Footnote 7mThere are various rules affecting the scope of an _ that we won't go over here. See the Scala
Language Specification, section 6.23 for the full details. Generally, if you have to think about how an
expression involving _'s will be interpreted, it's better to just use the named parameter syntax, as in x => x
+ 1.
www.it-ebooks.info
26
www.it-ebooks.info
27
The details of the algorithm aren't too important here. What is important is that
the code for binarySearch is going to look almost identical if we are searching
for a Double in an Array[Double], an Int in an Array[Int], a String
in an Array[String], or an A in an Array[A]. We can write
binarySearch more generally for any type A, by accepting a function to use for
testing whether an A value is greater than another:
www.it-ebooks.info
28
The type parameter list introduces type variables (or sometimes type
parameters) that can be referenced in the rest of the type signature (exactly
analogous to how variables introduced in the arguments to a function can be
referenced in the body of the function). Here, the type variable A is referenced in
three places—the search key is required to have the type A, the values of the array
are required to have the type A (since it is an Array[A]), and the gt function
must accept two arguments both of type A (since it is an (A,A) => Boolean).
The fact that the same type variable is referenced in all three places in the type
signature enforces that the type must be the same for all three arguments, and the
compiler will enforce this fact anywhere we try to call binarySearch. If we try
to search for a String in an Array[Int], for instance, we'll get a type
mismatch error.9
Footnote 9mUnfortunately, Scala's use of subtyping means we sometimes get rather cryptic compile errors,
since Scala will try to find a common supertype to use for the A type parameter, and will fall back to using
Any, the supertype of all types.
www.it-ebooks.info
29
As you might have seen when writing isSorted, the universe of possible
implementations is significantly reduced when implementing a polymorphic
function. If a function is polymorphic in some type, A, the only operations that can
be performed on that A are those passed into the function as arguments (or that can
be defined in terms of these given operations).10 In some cases, you'll find that the
universe of possibilities for a given polymorphic type is constrained such that there
is only a single implementation!
Footnote 10mTechnically, all values in Scala can be compared for equality (using ==), and we can compute a
hash code for them as well. But this is something of a wart inherited from Java.
Let's look at an example of this, a higher-order function for doing what is called
partial application. This function, partial1, takes a value and a function of two
arguments, and returns a function of one argument as its result. The name comes
from the fact that the function is being applied to some but not all of its required
arguments.
www.it-ebooks.info
30
concrete types here, so we can only stick things together using the local 'rules of
the universe' established by the type signature. The style of reasoning required here
is very common in functional programming—we are simply manipulating symbols
in a very abstract way, similar to how we would reason when solving an algebraic
equation.
EXERCISE 4 (hard): Let's look at another example, currying, which converts a
function of N arguments into a function of one argument that returns another
function as its result.11 Here again, there is only one implementation that
typechecks.
Footnote 11mThis is named after the mathematician Haskell Curry, who discovered the principle. It was
independently discovered earlier by Moses Schoenfinkel, but "Schoenfinkelization" didn't catch on.
Let's look at a final example, function composition, which feeds the output of
one function in as the input to another function. Again, the implementation of this
function is fully determined by its type signature.
EXERCISE 6: Implement the higher-order function that composes two
functions.
This is such a common thing to want to do that Scala's standard library provides
compose as a method on Function1. To compose two functions f and g, you
simply say f compose g12. It also provides an andThen method. f
andThen g is the same as g compose f:
Footnote 12mSolving the compose exercise by using this library function is considered cheating.
www.it-ebooks.info
31
Interestingly, functions like compose do not care whether they are operating
on huge functions backed by millions of lines of code, or a couple of one-line
functions. Polymorphic, higher-order functions often end up being extremely
widely applicable, precisely because they say nothing about any particular domain
and are simply abstracting over a common pattern that occurs in many contexts.
We'll be writing many more such functions over the course of this book, and this is
just a short taste of the style of reasoning and thinking you'll use when writing such
functions.
2.7 Conclusion
In this chapter we have learned some preliminary functional programming
concepts, and enough Scala to get going. We learned how to define simple
functions and programs, including how we can express loops using recursion, then
introduced the idea of higher-order functions and got some practice writing
polymorphic functions in Scala. We saw how the implementations of polymorphic
functions are often significantly constrained, such that one can often simply 'follow
the types' to the correct implementation. This is something we'll see a lot more of
in the chapters ahead.
Although we haven't yet written any large or complex programs, the principles
we have discussed here are scalable and apply equally well to programming in the
large as they do to programming in the small.
Next up we will look at using pure functions to manipulate data.
www.it-ebooks.info
32
Index Terms
annonymous function
block
curried form
currying
function literals
higher-order function
import
lambda
lambda expression
lambda notation
left-hand side
method definition
method signature
module
monomorphic
monomorphism
namespace
object
package
partial application
proper tail-calls
REPL
right-hand side
self-recursion
singleton type
string interpolation
tail-call optimization
tail position
type parameters
uncurry
uncurry
underscore syntax
val keyword
www.it-ebooks.info
33
3.1 Introduction
Functional data structures
3
We said in the introduction that functional programs do not update variables or
modify data structures. This raises pressing questions—what sort of data structures
can we use in functional programming, how do we define them in Scala, and how
do we operate over these data structures? In this chapter we will learn the concept
of a functional data structure and how to define and work with such structures.
We'll use this as an opportunity to introduce how data types are defined in
functional programming, learn about the related technique of pattern matching, and
get practice writing and generalizing pure functions.
This chapter has a lot of exercises, particularly to help with this last
point—writing and generalizing pure functions. Some of these exercises may be
challenging. As always, if you need to, consult the hints or the answers, or ask for
help online.
www.it-ebooks.info
34
Somewhat surprisingly, the answer is 'no'. We will return to this issue after
examining the definition of what is perhaps the most ubiquitous of functional data
structures, the singly-linked list. The definition here is identical in spirit to (though
simpler than) the List data type defined in Scala's standard library. This code
listing makes use of a lot of new syntax and concepts, so don't worry if not
everything makes sense at first—we will talk through it in detail.1
Footnote 1mNote—the implementations of sum and product here are not tail recursive. We will be writing
tail recursive versions of these functions later in the chapter.
package fpinscala.datastructures
object List {
def sum(ints: List[Int]): Int = ints match {
case Nil => 0
case Cons(x,xs) => x + sum(xs)
}
www.it-ebooks.info
35
Let's look first at the definition of the data type, which begins with the
keywords sealed trait. In general, we introduce a data type with the trait
keyword. A trait is an abstract interface that may optionally contain
implementations of some methods. Here we are declaring a trait, called List,
with no methods on it. Adding sealed in front means that all implementations of
our trait must be declared in this file.2
Footnote 2mWe could also say abstract class here instead of trait. Technically, an abstract
class can contain constructors, in the OO sense, which is what separates it from a trait, which cannot
contain constructors. This distinction is not really relevant for our purposes right now.
Just as functions can be polymorphic, data types can be as well, and by adding
the type parameter [+A] after sealed trait List and then using that A
parameter inside of the Cons data constructor, we have declared the List data
type to be polymorphic in the type of elements it contains, which means we can
use this same definition for a list of Int elements (denoted List[Int]),
Double elements (denoted List[Double]), String elements (
List[String]), and so on (the + indicates that the type parameter, A is
covariant—see sidebar 'More about variance' for more info).
A data constructor declaration gives us a function to construct that form of the
data type (the case object Nil lets us write Nil to construct an empty List
, and the case class Cons lets us write Cons(1, Nil), Cons(1,
Cons(2, Nil)), and so on for nonempty lists), but also introduces a pattern
that can be used for pattern matching, as in the functions sum and product.
www.it-ebooks.info
36
As you might expect, the sum function states that the sum of an empty list is 0,
www.it-ebooks.info
37
and the sum of a nonempty list is the first element, x , plus the sum of the
remaining elements, xs.4 Likewise the product definition states that the product
of an empty list is 1.0, the product of any list starting with 0.0 is 0.0,5 and the
product of any other nonempty list is the first element multiplied by the product of
the remaining elements. Notice these are recursive definitions, which are quite
common when writing functions that operate over recursive data types like List
(which refers to itself recursively in its Cons data constructor).
Footnote 4mWe could call x and xs anything there, but it is a common convention to use xs, ys, as, bs as
variable names for a sequence of some sort, and x, y, z, a, or b as the name for a single element of a
sequence. Another common naming convention is h for the first element of a list (the "head" of the list), t for
the remaining elements (the "tail"), and l for an entire list.
Footnote 5mLISTS
6This isn't the most robust test—pattern matching on 0.0 will match only the
exact value 0.0, not 1e-102 or any other value very close to 0.
Footnote 6mLISTS
Pattern matching works a bit like a fancy switch statement that may descend
into the structure of the expression it examines and extract subexpressions of that
structure (we'll explain this shortly). It is introduced with an expression (the target
or scrutinee), like ds followed by the keyword match, and a {}-wrapped
sequence of cases. Each case in the match consists of a pattern (like
Cons(x,xs)) to the left of the => and a result (like x * product(xs)) to
the right of the =>. If the target matches the pattern in a case (see below), the result
of that case becomes the result of the entire match expression. If multiple patterns
match the target, Scala chooses the first matching case.
www.it-ebooks.info
38
List(1,2,3) match { case _ => 42 } results in 42. Here we are using a variable
pattern, _, which matches any expression. We could say x or foo instead of _ but we
usually use _ to indicate a variable whose value we ignore in the result of the case.8
Footnote 8mThe _ variable pattern is treated somewhat specially in that it may be mentioned multiple
times in the pattern to ignore multiple parts of the target.
List(1,2,3) match { case Cons(h,t) => h } results in 1. Here we are using a data
constructor pattern in conjunction with variables to capture or bind a subexpression of
the target.
List(1,2,3) match { case Cons(_,t) => t } results in List(2,3).
List(1,2,3) match { case Nil => 42 } results in a MatchError at runtime. A
MatchError indicates that none of the cases in a match expression matched the target.
www.it-ebooks.info
39
pattern matches the target if there exists an assignment of variables in the pattern to
subexpressions of the target that make it structurally equivalent to the target. The
result expression for a matching case will then have access to these variable
assignments in its local scope.
EXERCISE 1: What will the result of the following match expression be?
You are strongly encouraged to try experimenting with pattern matching in the
REPL to get a sense for how it behaves.
www.it-ebooks.info
40
In the same way, to "remove" an element from the front of a list val mylist
= Cons(x,xs), we simply return xs. There is no real removing going on. The
original list, mylist is still available, unharmed. We say that functional data
structures are persistent, meaning that existing references are never changed by
operations on the data structure.
Let's try implementing a few different functions for "modifying" lists in
different ways. You can place this and other functions we write inside the List
companion object.
EXERCISE 2: Implement the function tail for "removing" the first element
of a List. Notice the function takes constant time. What are different choices you
could make in your implementation if the List is Nil? We will return to this
question in the next chapter.
EXERCISE 3: Generalize tail to the function drop, which removes the first
n elements from a list.
EXERCISE 4: Implement dropWhile,10 which removes elements from the
List prefix as long as they match a predicate. Again, notice these functions take
time proportional only to the number of elements being dropped—we do not need
to make a copy of the entire List.
Footnote 10mdropWhile has two argument lists to improve type inference. See sidebar.
www.it-ebooks.info
41
EXERCISE 5: Using the same idea, implement the function setHead for
replacing the first element of a List with a different value.
Data sharing often brings some more surprising efficiency gains. For instance,
here is a function that adds all the elements of one list to the end of another:
Notice that this definition only copies values until the first list is exhausted, so
www.it-ebooks.info
42
its runtime is determined only by the length of a1. The remaining list then just
points to a2. If we were to implement this same function for two arrays, we would
be forced to copy all the elements in both arrays into the result.
EXERCISE 6: Not everything works out so nicely. Implement a function,
init, which returns a List consisting of all but the last element of a List. So,
given List(1,2,3,4), init will return List(1,2,3). Why can't this
function be implemented in constant time like tail?
Because of the structure of a singly-linked list, any time we want to replace the
tail of a Cons, even if it is the last Cons in the list, we must copy all the
previous Cons objects. Writing purely functional data structures that support
different operations efficiently is all about finding clever ways to exploit data
sharing, which often means working with more tree-like data structures. We are not
going to cover these data structures here; for now, we are content to use the
functional data structures others have written. As an example of what's possible, in
the Scala standard library, there is a purely functional sequence implementation,
Vector (documentation link), with constant-time random access, updates, head,
tail, init, and constant-time additions to either the front or rear of the
sequence.
Notice how similar these two definitions are. The only things that differ are the
www.it-ebooks.info
43
value to return in the case that the list is empty (0 in the case of sum, 1.0 in the
case of product), and the operation to apply to combine results (+ in the case of
sum, * in the case of product). Whenever you encounter duplication like this, as
we've discussed before, you can generalize it away by pulling subexpressions out
into function arguments. If a subexpression refers to any local variables (the +
operation refers to the local variables x and xs introduced by the pattern, similarly
for product), turn the subexpression into a function that accepts these variables
as arguments. Putting this all together for this case, our function will take as
arguments the value to return in the case of the empty list, and the function to add
an element to the result in the case of a nonempty list:12
Footnote 12mIn the Scala standard library, foldRight is a method on List and its arguments are curried
similarly for better type inference.
Again, placing f in its own argument group after l and z lets type inference
determine the input types to f. See earlier sidebar.
foldRight is not specific to any one type of element, and the value that is
returned doesn't have to be of the same type as the elements either. One way of
describing what foldRight does is that it replaces the constructors of the list,
Nil and Cons with z and f , respectively. So the value of
foldRight(Cons(a, Nil), z)(f) becomes f(a, z) , and
foldRight(Cons(a, Cons(b, Nil)), z)(f) becomes f(a, f(b,
z)).
Let's look at an example. We are going to trace the evaluation of
foldRight(Cons(1, Cons(2, Cons(3, Nil))), 0)(_ + _), by
repeatedly subsituting the definition of foldRight in for its usages. We'll use
www.it-ebooks.info
44
Notice that foldRight must traverse all the way to the end of the list
(pushing frames onto the call stack as we go) before it can begin collapsing it.
EXERCISE 7: Can product implemented using foldRight immediately
halt the recursion and return 0.0 if it encounters a 0.0? Why or why not?
Consider how any short-circuiting might work if you call foldRight with a
large list. This is a deeper question that we'll return to a few chapters from now.
EXERCISE 8: See what happens when you pass Nil and Cons themselves to
foldRight , like this: foldRight(List(1,2,3),
Nil:List[Int])(Cons(_,_)).13 What do you think this says about the
relationship between foldRight and the data constructors of List?
Footnote 13mThe type annotation Nil:List[Int] is needed here, because otherwise Scala infers the B
type parameter in foldRight as List[Nothing].
EXERCISE 11: Write sum, product, and a function to compute the length of
a list using foldLeft.
www.it-ebooks.info
45
EXERCISE 12: Write a function that returns the reverse of a list (so given
List(1,2,3) it returns List(3,2,1)). See if you can write it using a fold.
EXERCISE 13 (hard): Can you write foldLeft in terms of foldRight?
How about the other way around?
EXERCISE 14: Implement append in terms of either foldLeft or
foldRight.
EXERCISE 15 (hard): Write a function that concatenates a list of lists into a
single list. Its runtime should be linear in the total length of all lists. Try to use
functions we have already defined.
www.it-ebooks.info
46
EXERCISE 19: Write a function filter that removes elements from a list
unless they satisfy a given predicate. Use it to remote all odd numbers from a
List[Int].
EXERCISE 20: Write a function flatMap, that works like map except that
the function given will return a list instead of a single result, and that list should be
inserted into the final resulting list. Here is its signature:
def take(n: Int): List[A]: returns a list consisting of the first n elements of this.
def takeWhile(f: A => Boolean): List[A]: returns a list consisting of the longest
valid prefix of this whose elements all pass the predicate f.
def forall(f: A => Boolean): Boolean: returns true if and only if all elements of
this pass the predicate f.
def exists(f: A => Boolean): Boolean: returns true if any element of this passes
the predicate f.
www.it-ebooks.info
47
scanLeft and scanRight are like foldLeft and foldRight, but they return the List of
partial results, rather than just the final accumulated value.
One of the problems with List is that while we can often express operations
and algorithms in terms of very general-purpose functions, the resulting
implementation isn't always efficient—we may end up making multiple passes
over the same input, or else have to write explicit recursive loops to allow early
termination.
EXERCISE 24 (hard): As an example, implement hasSubsequence for
checking whether a List contains another List as a subsequence. For instance,
List(1,2,3,4) would have List(1,2), List(2,3), and List(4) as
subsequences, among others. You may have some difficulty finding a concise
purely functional implementation that is also efficient. That's okay. Implement the
function however comes most naturally. We will return to this implementation in a
couple of chapters and hopefully improve on it. Note: any two values, x, and y,
can be compared for equality in Scala using the expression x == y.
3.5 Trees
List is just one example of what is called an algebraic data type (ADT).
(Somewhat confusingly, ADT is sometimes used in OO to stand for "abstract data
type".) An ADT is just a data type defined by one or more data constructors, each
of which may contain zero or more arguments. We say that the data type is the sum
or union of its data constructors, and each data constructor is the product of its
arguments, hence the name algebraic data type.16
Footnote 16mThe naming is not coincidental. There is actually a deep connection, beyond the scope of this
book, between the "addition" and "multiplication" of types to form an ADT and addition and multiplication of
numbers.
When you encode a data type as an ADT, the data constructors and associated
patterns form part of that type's API, and other code may be written in terms of
explicit pattern
www.it-ebooks.info
48
scala> p._1
res0: java.lang.String = Bob
scala> p._2
res1: Int = 42
Algebraic data types can be used to define other data structures. Let's define a
simple binary tree data structure:
www.it-ebooks.info
49
name on List, that modifies each element in a tree with a given function.
We do typically use ADTs for cases where the set of cases is closed
(known to be fixed). For List and Tree, changing the set of data
constructors would significantly change what these data types are. List
is a singly-linked list—that is its nature—and the two cases, Nil and
Cons form part of its useful public API. We can certainly write code
which deals with a more abstract API than List (we will see examples
of this later in the book), but this sort of information hiding can be
handled as a separate layer rather than being baked into List directly.
EXERCISE 29: Generalize size, maximum, depth, and map, writing a new
function fold that abstracts over their similarities. Reimplement them in terms of
this more general function. Can you draw an analogy between this fold function
and the left and right folds for List?
3.6 Summary
In this chapter we covered a number of important concepts. We introduced
algebraic data types and pattern matching and showed how to implement purely
functional data structures, including the singly-linked list. Also, through the
exercises in this chapter, we hope you got more comfortable writing pure functions
and generalizing them. We will continue to develop this skill in the chapters ahead.
18
Footnote 18mAs you work through more of the exercises, you may want to read appendix Todo discussing
different techniques for generalizing functions.
www.it-ebooks.info
50
Index Terms
companion object
companion object
covariant
data constructor
data sharing
functional data structure
match expression
pattern
pattern
pattern matching
pattern matching
persistence
program trace
tracing
variance
zip
zipWith
www.it-ebooks.info
51
4.1 Introduction
Handling errors without exceptions
4
In chapter 1 we said that throwing an exception breaks referential transparency.
Let's look at an example:
www.it-ebooks.info
52
Seq is the common interface of various linear sequence-like collections. Check the
API docs for more information.
sum is defined as a method on Seq using some magic (that we won't get into here)
that makes the method available only if the elements of the sequence are numeric.
The first possibility is to return some sort of bogus value of type Double. We
could simply return xs.sum / xs.length in all cases, and have it result in
0.0/0.0 when the input is empty, which is Double.NaN, or we could return
some other sentinel value. In other situations we might return null instead of a
value of the needed type. We reject this solution for a few reasons:
It allows errors to silently propagate—the caller can forget to check this condition and
will not be alerted by the compiler, which might result in subsequent code not working
properly. Often the error won't be detected until much later in the code.
It is not applicable to polymorphic code. For some output types, we might not even have
a sentinel value of that type even if we wanted to! Consider a function like max which
finds the maximum value in a sequence according to a custom comparison function: def
max[A](xs: Seq[A])(greater: (A,A) => Boolean): A. If the input were empty, we
cannot invent a value of type A. Nor can null be used here since null is only valid for
non-primitive types, and A is completely unconstrained by this signature.
It demands a special policy or calling convention of callers—proper use of the mean
function now requires that callers do something other than simply call mean and make use
of the result. Giving functions special policies like this makes it difficult to pass them to
www.it-ebooks.info
53
higher-order functions, who must treat all functions they receive as arguments uniformly
and will generally only be aware of the types of these functions, not any special policy or
calling convention.
The second possibility is to force the caller to supply an argument which tells
us what to do in case we don't know how to handle the input:
This makes mean into a total function, but it has drawbacks—it requires that
immediate callers have direct knowledge of how to handle the undefined case and
limits them to returning a Double. What if mean is called as part of a larger
computation and we would like to abort that computation if mean is undefined? Or
perhaps we would like to take some completely different branch in the larger
computation in this case? Simply passing an onEmpty parameter doesn't give us
this freedom.
We need a way to defer the decision of how to handle undefined cases so that
they can be dealt with at the most appropriate level.
Option has two cases: it can be defined, in which case it will be a Some, or it
can be undefined, in which case it will be None. We can use this for our definition
of mean like so:
The return type now reflects the possibility that the result is not always defined.
www.it-ebooks.info
54
These aren't the only examples—we'll see Option come up in many different
situations. What makes Option convenient is that we can factor out common
patterns of error handling via higher order functions, freeing us from writing the
usual boilerplate that comes with exception-handling code.
Option can be thought of like a List that can contain at most one element,
and many of the List functions we saw earlier have analogous functions on
Option. Let's look at some of these functions. We are going to do something
slightly different than last chapter. Last chapter we put all the functions that
operated on List in the List companion object. Here we are going to place our
functions, when possible, inside the body of the Option trait, so they can be
called with OO syntax (obj.fn(arg1) or obj fn arg1 instead of fn(obj,
arg1)). This is a stylistic choice with no real significance, and we'll use both
styles throughout this book.2
Footnote 2mIn general, we'll use the OO style where possible for functions that have a single, clear operand
(like List.map), and the standalone function style otherwise.
trait Option[+A] {
def map[B](f: A => B): Option[B]
def flatMap[B](f: A => Option[B]): Option[B]
def getOrElse[B >: A](default: => B): B
def orElse[B >: A](ob: => Option[B]): Option[B]
def filter(f: A => Boolean): Option[A]
}
There are a couple new things here. The default: => B type annotation in
getOrElse (and the similar annotation in orElse) indicates the argument will
www.it-ebooks.info
55
not be evaluated until it is needed by the function. Don't worry about this for
now—we'll talk much more about it in the next chapter. Also, the B>:A parameter
on these functions indicates that B must be a supertype of A. It's needed to
convince Scala that it is still safe to declare Option[+A] as covariant in A
(which lets the compiler assume things like Option[Dog] is a subtype of
Option[Animal]). Likewise for orElse. This isn't really important to our
purposes; it's mostly an artifact of the OO style of placing the functions that
operate on a type within the body of the trait.
EXERCISE 1: We'll explore when you'd use each of these next. But first, as an
exercise, implement all of the above functions on Option. As you implement
each function, try to think about what it means and in what situations you'd use it.
Here are a few hints:
It is fine to use pattern matching, though you should be able to implement all the
functions besides map and getOrElse without resorting to pattern matching.
For map and flatMap, the type signature should be sufficient to determine the
implementation.
getOrElse returns the result inside the Some case of the Option, or if the Option is None,
returns the given default value.
orElse returns the first Option if it is defined, otherwise, returns the second Option.
A dictionary or associative container with String as the key type and Employee as
www.it-ebooks.info
56
filter can be used to convert successes into failures if the successful values
don't match the given predicate. A common pattern is to transform an Option via
calls to map, flatMap, and/or filter, then use getOrElse to do error
handling at the end.
www.it-ebooks.info
57
As an example of when you might use map, let's look at one more example of a
function that returns Option:
import java.util.regex._
This example uses the Java standard library's regex package to parse a string
into a regular expression pattern.4 If there is a syntax error in the pattern (it's not a
valid regular expression), we catch the exception thrown by the library function
and return None. Methods on the Pattern class don't need to know anything
about Option. We can simply lift them using the map function:
Footnote 4mScala runs on the Java Virtual Machine and is completely compatible with all existing Java
libraries. We can therefore call Pattern.compile, a Java function, exactly as we would any Scala
function.
www.it-ebooks.info
58
The details of this API don't matter too much, but p.matcher(s).matches will check
if the string s matches the pattern p.
So far we are only lifting functions that take one argument. But some functions
take more than one argument and we would like to be able to lift them too. The
for-comprehension makes this easy, and we can combine as many options as we
want:
www.it-ebooks.info
59
Sometimes we will want to map over a list using a function that might fail,
returning None if applying it to any element of the list returns None. For example,
parsing a whole list of strings into a list of patterns. In that case, we can simply
sequence the results of the map:
Unfortunately, this is a little inefficient, since it traverses the list twice. Wanting
to sequence the results of a map this way is a common enough occurrence to
www.it-ebooks.info
60
Either has only two cases, just like Option. The essential difference is that
both cases carry a value. The Either data type represents, in a very general way,
values that can be one of two things. We can say that it is a disjoint union of two
types. When we use it to indicate success or failure, by convention the Left
constructor is reserved for the failure case.6
Footnote 6mEither is also often used more generally to encode one of two possibilities, in cases where it
isn't worth defining a fresh data type. We'll see some examples of this throughout the book.
www.it-ebooks.info
61
Let's look at the mean example again, this time returning a String in case of
failure:
Sometimes we might want to include more information about the error, for
example a stack trace showing the location of the error in the source code. In such
cases we can simply return the exception in the Left side of an Either:
www.it-ebooks.info
62
for {
age <- Right(42)
name <- Left("invalid name")
salary <- Right(1000000.0)
} yield employee(name, age, salary)
www.it-ebooks.info
63
4.4 Conclusion
Using algebraic data types such as Option and Either, we can handle errors in
a way that is modular, compositional, and simple to reason about. In this chapter,
we have developed a number of higher-order functions that manipulate errors in
ways that we couldn't otherwise if we were just throwing exceptions. With these
new tools in hand, exceptions should be reserved only for truly unrecoverable
conditions in our programs.
Index Terms
disjoint union
for-comprehension
lifting
lifting
www.it-ebooks.info
64
In this expression, map (_ + 10) will produce an intermediate list that then
gets passed to filter (_ % 2 == 0), which in turn constructs a list which
gets passed to map (_ * 3) which then produces the final list. In other words,
each transformation will produce a temporary list that only ever gets used as input
to the next transformation and is then immediately discarded.
Think about how this program will be evaluated. If we manually produce a
trace of its evaluation, the steps would look something like this:
www.it-ebooks.info
65
Here we are showing the result of each substitution performed to evaluate our
expression (for example, to go from the first line to the second, we have substituted
List(1,2,3,4) map (_ + 10) with List(11,12,13,14), based on
the definition of map).2 This view makes it clear how the calls to map and
filter each perform their own traversal of the input and allocate lists for the
output. Wouldn't it be nice if we could somehow fuse sequences of transformations
like this into a single pass and avoid creating temporary data structures? We could
rewrite the code into a while-loop by hand, but ideally we'd like to have this done
automatically while retaining the same high-level compositional style. We want to
use higher-order functions like map and filter instead of manually fusing
passes into loops.
Footnote 2mWith program traces like these, it is often more illustrative to not fully trace the evaluation of
every subexpression. For instance, in this case, we've omitted the full expansion of List(1,2,3,4) map
(_ + 10). We could "enter" the definition of map and trace its execution but we chose to omit this level of
detail in this trace.
It turns out that we can accomplish this through the use of non-strictness (or
more informally, "laziness"). In this chapter, we will explain what exactly this
means, and we'll work through the implementation of a lazy list type that fuses
sequences of transformations. Although building a "better" list is the motivation for
this chapter, we'll see that non-strictness is a fundamental technique for improving
on the efficiency and modularity of functional programs in general.
www.it-ebooks.info
66
means that the function may choose not to evaluate one or more of its arguments.
In contrast, a strict function always evaluates its arguments. Strict functions are the
norm in most programming languages and most languages don't even provide a
way to define non-strict functions. Unless you tell it otherwise, any function
definition in Scala will be strict (and all the functions we have defined so far have
been strict). As an example, consider the following function:
When you invoke square(41.0 + 1.0) square will receive the evaluated
value of 42.0 because it is strict. If you were to invoke
square(sys.error("failure")), you would get an exception, since the
sys.error("failure") expression will be evaluated before entering the
body of square.
Although we haven't yet learned the syntax for indicating non-strictness in
scala, you are almost certainly familiar with non-strictness as a concept. For
example, the Boolean functions && and || are non-strict. You may be used to
thinking of && and || as built-in syntax, part of the language, but we can also
think of them as functions that may choose not to evaluate their arguments. The
function && takes two Boolean arguments, but only evaluates the second
argument if the first is true:
www.it-ebooks.info
67
return in the case that the condition is true, and another expression of the same type
A to return if the condition is false. This if function would be non-strict, since it
will not evaluate all of its arguments. To be more precise, we would say that the
if function is strict in its condition parameter, since it will always evaluate the
condition to determine which branch to take, and non-strict in the two branches for
the true and false cases, since it will only evaluate one or the other based on
the condition.
In Scala, we can write non-strict functions by accepting some of our arguments
unevaluated, using the following syntax:
The arguments we would like to pass unevaluated have => immediately before
their type. In the body of the function, we do not need to do anything to evaluate an
argument annotated with =>. We just reference the identifier. We also call this
function with the usual function call syntax:
Scala will take care of making sure that the second and third arguments are
passed unevaluated to the body of if2.3
Footnote 3mThe unevaluated form of an expression is often called a thunk. Thunks are represented at runtime
in Scala as a value of type scala.Function0, which you can see if you're curious by inspecting the
signature of non-strict functions in the .class file the Scala compiler generates.
www.it-ebooks.info
68
Adding the lazy keyword to a val declaration will cause Scala to delay
evaluation of the right hand side until it is first referenced and will also cache the
result so that subsequent references of j don't trigger repeated evaluation. In this
example, we were going to evaluate i on the next line anyway, so we could have
just written val j = i. Despite receiving its argument unevaluated, pair2 is
still considered a strict function since it always ends up evaluating its argument. In
other situations, we can use lazy val when we don't know if subsequent code
will evaluate the expression and simply want to cache the result if it is ever
demanded.
As a final bit of terminology, a non-strict function that evaluates its arguments
each time it references them is said to evaluate those arguments by name ; if it
evaluates them only once and then caches their value, it is said to evaluate by need,
or it's said to be lazy. We'll often refer to unevaluated parameters in Scala as
by-name parameters. Note also that the terms laziness or lazy evaluation are
sometimes used informally to refer to any sort of non-strict evaluation, not
necessarily evaluation by need. When you encounter the word "lazy" in this book,
you can assume that we are using an informal definition.
www.it-ebooks.info
69
trait Stream[+A] {
def uncons: Option[(A, Stream[A])]
def isEmpty: Boolean = uncons.isEmpty
}
object Stream {
Notice the cons function is non-strict in its arguments. Thus the head and tail
of the stream will not be evaluated until first requested. We'll sometimes write
cons using the infix, right associative operator #::, so 1 #:: 2 #:: empty
is equivalent to cons(1, cons(2, empty)). We could add it to the Stream
trait in the same way that we added :: to the List trait in the code for chapter 3,
though there are some annoying additional hoops to jump through to make it
properly non-strict. See the associated code for this chapter if you're interested in
exactly how this syntax is implemented.
Before continuing, let's write a few helper functions to make inspecting streams
easier.
EXERCISE 1: Write a function to convert a Stream to a List, which will
force its evaluation and let us look at it in the REPL. You can convert to the
regular List type in the standard library. You can place this and other functions
that accept a Stream inside the Stream trait.
www.it-ebooks.info
70
You can use take and toList together to inspect the streams we'll be
creating. For example, try printing Stream(1,2,3).take(2).toList.
This looks very similar to the foldRight we wrote for List, but notice how
our combining function, f, is non-strict in its second parameter. If f chooses not to
evaluate its second parameter, this terminates the traversal early. We can see this
by using foldRight to implement exists, which checks to see if any value in
the Stream matches a given predicate.
www.it-ebooks.info
71
Stream(1,2,3,4).map(_ + 10).filter(_ % 2 == 0)
(11 #:: Stream(2,3,4).map(_ + 10)).filter(_ % 2 == 0)
Stream(2,3,4).map(_ + 10).filter(_ % 2 == 0)
(12 #:: Stream(3,4).map(_ + 10)).filter(_ % 2 == 0)
12 #:: Stream(3,4).map(_ + 10).filter(_ % 2 == 0)
12 #:: (13 #:: Stream(4).map(_ + 10)).filter(_ % 2 == 0)
12 #:: Stream(4).map(_ + 10).filter(_ % 2 == 0)
12 #:: (14 #:: Stream().map(_ + 10)).filter(_ % 2 == 0)
12 #:: 14 #:: Stream().map(_ + 10).filter(_ % 2 == 0)
12 #:: 14 #:: Stream()
www.it-ebooks.info
72
instantiate the intermediate stream that results from the map. For this reason,
people sometimes describe streams as "first-class loops" whose logic can be
combined using higher-order functions like map and filter.
The incremental nature of stream transformations also has important
consequences for memory usage. In a sequence of stream transformations like this,
the garbage collector can usually reclaim the space needed for each intermediate
stream element, as soon as that element is passed on to the next transformation.
Here, for instance, the garbage collector can reclaim the space allocated for the
value 13 emitted by map as soon as filter determines it isn't needed. Of course,
this is a simple example; in other situations we might be dealing with larger
numbers of elements, and the stream elements themselves could be large objects
that retain significant amounts of memory. Being able to reclaim this memory as
quickly as possible can cut down on the amount of memory required by your
program as a whole.5
Footnote 5mWe will have a lot more to say about defining memory-efficient streaming calculations, in
particular calculations that require I/O, in part 4 of this book.
Although ones is infinite, the functions we've written so far only inspect the
portion of the stream needed to generate the demanded output. For example:
scala> ones.take(5).toList
res0: List[Int] = List(1, 1, 1, 1, 1)
scala> ones.exists(_ % 2 != 0)
res1: Boolean = true
ones.map(_ + 1).exists(_ % 2 == 0)
ones.takeWhile(_ == 1)
ones.forAll(_ != 1)
www.it-ebooks.info
73
In each case, we get back a result immediately. Be careful though, since it's
easy to write an expression that never terminates. For example ones.forAll(_
== 1) will forever need to inspect more of the series since it will never encounter
an element that allows it to terminate with a definite answer.
Let's see what other functions we can discover for generating streams.
EXERCISE 7: Generalize ones slightly to the function constant which
returns an infinite Stream of a given value.
Option is used to indicate when the Stream should be terminated, if at all. The
function unfold is the most general Stream-building function. Notice how
closely it mirrors the structure of the Stream data type.
unfold and the functions we can implement with it are examples of what is
sometimes called a corecursive function. While a recursive function consumes data
and eventually terminates, a corecursive function produces data and coterminates.
We say that such a function is productive, which just means that we can always
evaluate more of the result in a finite amount of time (for unfold, we just need to
run the function f one more time to generate the next element). Corecursion is also
sometimes called guarded recursion. These terms aren't that important to our
www.it-ebooks.info
74
discussion, but you will hear them used sometimes in the context of functional
programming. If you are curious to learn where they come from and understand
some of the deeper connections, follow the references in the chapter notes.
EXERCISE 11: Write fibs, from, constant, and ones in terms of
unfold.
EXERCISE 12: Use unfold to implement map, take, takeWhile, zip (as
in chapter 3), and zipAll. The zipAll function should continue the traversal as
long as either stream has more elements — it uses Option to indicate whether
each stream has been exhausted.
Now that we have some practice writing stream functions, let's return to the
exercise we covered at the end of chapter 3 — a function, hasSubsequence, to
check whether a list contains a given subsequence. With strict lists and
list-processing functions, we were forced to write a rather tricky monolithic loop to
implement this function without doing extra work. Using lazy lists, can you see
how you could implement hasSubsequence by combining some other
functions we have already written? Try to think about it on your own before
continuing.
EXERCISE 13 (hard): implement startsWith using functions you've
written. It should check if one Stream is a prefix of another. For instance,
Stream(1,2,3) starsWith Stream(1,2) would be true.
www.it-ebooks.info
75
5.5 Summary
In this chapter we have introduced non-strictness as a fundamental technique for
implementing efficient and modular functional programs. As we have seen, while
non-strictness can be thought of as a technique for recovering some efficiency
when writing functional code, it's also a much bigger idea — non-strictness can
improve modularity by separating the description of an expression from the "how
and when" of its evaluation. Keeping these concerns separate lets us reuse a
description in multiple contexts, evaluating different portions of our expression to
obtain different results. We were not able to do that when description and
evaluation were intertwined as they are in strict code. We saw a number of
examples of this principle in action over the course of the chapter and we will see
many more in the remainder of the book.
Index Terms
non-strict
strict
thunk
www.it-ebooks.info
76
scala> rng.nextDouble
res1: Double = 0.9867076608154569
scala> rng.nextDouble
res2: Double = 0.8455696498024141
scala> rng.nextInt
res3: Int = -623297295
scala> rng.nextInt
res4: Int = 1989531047
www.it-ebooks.info
77
trait Random {
def nextInt: Int
def nextBoolean: Boolean
def nextDouble: Double
...
}
trait RNG {
def nextInt: (Int, RNG)
}
Should generate a random Int. We will later define other functions in terms of
nextInt.
Rather than returning only the generated random number (as is done in
java.util.Random) and updating some internal state by mutating it in place,
we return the random number and the new state, leaving the old state unmodified.
In effect, we separate the computing of the next state from the concern of
propagating that state throughout the program. There is no global mutable memory
being used—we simply return the next state back to the caller. This leaves the
caller of nextInt in complete control of what to do with the new state. Notice
that we are still encapsulating the state, in the sense that users of this API do not
need to know anything about the implementation of the random number generator
itself.
Here is a simple implementation using the same algorithm as
www.it-ebooks.info
78
object RNG {
def simple(seed: Long): RNG = new RNG {
def nextInt = {
val seed2 = (seed*0x5DEECE66DL + 0xBL) &
((1L << 48) - 1)
((seed2 >>> 16).asInstanceOf[Int],
simple(seed2))
}
}
}
This problem of making seemingly stateful APIs pure, and its solution, of
having the API compute the next state rather than actually mutate anything, is not
unique to random number generation. It comes up quite frequently, and we can
always deal with it in this same way.1
Footnote 1mThere is an efficiency loss that comes with computing next states using pure functions, because it
means we cannot actually mutate the data in place. (Here, it is not really a problem since the state is just a single
Long that must be copied.) This loss of efficiency can be mitigated by using efficient purely functional data
structures. It's also possible in some cases to mutate the data in place without breaking referential transparency.
We'll be discussing this in Part 4.
class Foo {
var s: FooState = ...
def bar: Bar
def baz: Int
}
Suppose bar and baz each mutate s in some way. We can mechanically
translate this to the purely functional API:
trait Foo {
def bar: (Bar, Foo)
def baz: (Int, Foo)
www.it-ebooks.info
79
Here, i1 and i2 will be the same! If we want to generate two distinct numbers,
we need to use the RNG returned by the first call to nextInt to generate the
second Int.
You can see the general pattern, and perhaps you can also see how it might get
somewhat tedious to use this API directly. Let's write a few functions to generate
random values and see if we notice any patterns we can factor out.
EXERCISE 1: Write a function to generate a random positive integer. Note:
you can use x.abs to take the absolute value of an Int, x. Make sure to handle
the corner case Int.MinValue, which doesn't have a positive counterpart.
www.it-ebooks.info
80
www.it-ebooks.info
81
We can now turn methods such as RNG's nextInt into values of this type:
We want to start writing combinators that let us avoid explicitly passing along
the RNG state. This will become a kind of domain-specific language that does all of
this passing for us. For example, a simple RNG-transition is the unit action,
which passes the RNG state through without using it, always returning a constant
value rather than a random value.
There is also map, for transforming the output of a state action without
modifying the state itself. Remember, Rand[A] is just a type alias for a function
type RNG => (A, RNG), so this is just a kind of function composition.
www.it-ebooks.info
82
www.it-ebooks.info
83
Here, State is short for "state action" (or even "state transition"). We might
even want to write it as its own class, wrapping the underlying function like this:
The representation doesn't matter too much. What is important is that we have a
single, general-purpose type and using this type we can write general-purpose
functions for capturing common patterns of handling and propagating state.
In fact, we could just make Rand a type alias for State:
EXERCISE 11: Generalize the functions unit, map, map2, flatMap, and
www.it-ebooks.info
84
sequence. Add them as methods on the State case class where possible.
Otherwise you should put them in a State companion object.
The functions we've written here capture only a few of the most common
patterns. As you write more functional code, you'll likely encounter other patterns
and discover other functions to capture them.
int.flatMap(x =>
int.flatMap(y =>
ints(x).map(xs =>
xs.map(_ % y))))
www.it-ebooks.info
85
It's not very clear what's going on here. But since we have map and flatMap
defined, we can use for-comprehension to recover the imperative style:
for {
x <- int
y <- int
xs <- ints(x)
} yield xs.map(_ % y)
This code is much easier to read (and write), and it looks like what it is—an
imperative program that maintains some state. But it is the same code. We get the
next Int and assign it to x, get the next Int after that and assign it to y, then
generate a list of length x, and finally return the list with all of its elements
wrapped around the modulus y.
To facilitate this kind of imperative programming with for-comprehensions
(or flatMaps), we really only need two primitive State combinators—one for
reading the state and one for writing the state. If we imagine that we have a
combinator get for getting the current state, and a combinator set for setting a
new state, we could implement a combinator that can modify the state in arbitrary
ways:
This method returns a State action that modifies the current state by the
function f. It yields Unit to indicate that it doesn't have a return value other than
the state.
EXERCISE 12: Come up with the signatures for get and set, then write their
implementations.
EXERCISE 13 (hard): To gain experience with the use of State, implement a
simulation of a simple candy dispenser. The machine has two types of input: You
can insert a coin, or you can turn the knob to dispense candy. It can be in one of
two states: locked or unlocked. It also tracks how many candies are left and how
many coins it contains.
www.it-ebooks.info
86
The method simulateMachine should operate the machine based on the list
of inputs and return the number of coins left in the machine at the end. Note that if
the input Machine has 10 coins in it, and a net total of 4 coins are added in the
inputs, the output will be 14.
6.5 Summary
In this chapter, we touched on the subject of how to deal with state and state
propagation. We used random number generation as the motivating example, but
the overall pattern comes up in many different domains, and this chapter illustrated
the basic idea of how to handle state in a purely functional way. The idea is very
simple: we use a pure function that accepts a state as its argument, and it returns
the new state alongside its result. Next time you encounter an imperative API that
relies on side effects, see if you can provide a purely functional version of it, and
use some of the functions we wrote here to make working with it more convenient.
www.it-ebooks.info
87
7.1 Introduction
Purely functional parallelism
7
In this chapter, we are going to build a library for creating and composing parallel
and asynchronous computations. We are going to work iteratively, refining our
design and implementation as we gain a better understanding of the domain and the
design space.
Before we begin, let's think back to the libraries we wrote in Part 1, for example
the functions we wrote for Option and Stream. In each case we defined a data
type and wrote a number of useful functions for creating and manipulating values
of that type. But there was something interesting about the functions we wrote. For
instance consider Stream—if you look back, you'll notice we wrote only a few
primitive functions (like foldRight and unfold) which required knowledge of
the internal representation of Stream (consisting of its uncons method). We
then wrote a large number of derived operations or combinators without
introducing additional primitives, just by combining existing functions.
In Part 1, very little design effort went into creating these nicely compositional
libraries. We created our data types and found, perhaps surprisingly, that it was
possible to define a large number of useful operations over these data types, just by
combining existing functions. When you create a library for a new domain, the
design process won't always be this easy. You will need to choose data types and
functions that facilitate this compositional structure, and this is what makes
functional design both challenging and interesting.
www.it-ebooks.info
88
www.it-ebooks.info
89
As we think about how what sort of data types and functions could enable
parallelizing this computation, we can shift our perspective. Rather than focusing
on how this parallelism will ultimately be implemented (likely using
java.lang.Thread and java.lang.Runnable and related types) and
forcing ourselves to work with those APIs directly, we are instead going to design
www.it-ebooks.info
90
our 'ideal' API as illuminated by our examples and work backwards from there to
an implementation.
Look at the line sum(l) + sum(r), which invokes sum on the two halves
recursively. Just from looking at this single line, we can see that whatever data
type we choose to represent our parallel computations needs to be able to contain a
result, that result will have some meaningful type (in this case Int), and we
require some way of extracting this result. Let's apply this newfound knowledge to
our implementation. For now, let's just invent a container type for our result,
Par[A] (for "parallel"), and assume the existence of the functions we need:
def unit[A](a: => A): Par[A], for taking an unevaluated A and returning a parallel
computation that yields an A.
def get[A](a: Par[A]): A, for extracting the resulting value from a parallel
computation.
Can we really just do this? Yes, of course! For now, we don't need to worry
about what other functions we require, what the internal representation of Par
might be, or how these functions are implemented. We are simply reading off the
needed data types and functions by inspecting our simple example. Let's update
this example now:
We've wrapped the two recursive calls to sum in calls to unit, and we are
calling get to extract the two results from the two subcomputations.
www.it-ebooks.info
91
Already, we can see a problem with both of these types—none of the methods
return a meaningful value. Therefore, if we want to get any information out of a
Runnable, it has to have some side effect, like mutating some internal state that
we know about. This is bad for composability—we cannot manipulate Runnable
objects generically, we need to always know something about their internal
behavior to get any useful information out of them. Thread also has the
downside that it maps directly onto operating system threads, which are a scarce
resource. We'd prefer to create as many 'logical' parallel computations as is
natural for our problem, and later deal with mapping these logical computations
onto actual OS threads.
We now have a choice about the meaning of unit and get—unit could
begin evaluating its argument immediately in a separate (logical) thread, 2 or it
could simply hold onto its argument until get is called and begin evaluation then.
But notice that in this example, if we want to obtain any degree of parallelism, we
require that unit begin evaluating its argument immediately. Can you see why?3
Footnote 2mWe'll use the term "logical thread" somewhat informally throughout this chapter, to mean a chunk
of computation that runs concurrent to the main execution thread of our program. There need not be a one-to-one
correspondence between logical threads and OS threads. We may have a large number of logical threads mapped
onto a smaller number of OS threads via thread pooling, for instance.
Footnote 3mFunction arguments in Scala are strictly evaluated from left to right, so if unit delays execution
until get is called, we will both spawn the parallel computation and wait for it to finish before spawning the
second parallel computation. This means the computation is effectively sequential!
But if unit begins evaluating its argument immediately, then calling get
arguably breaks referential transparency. We can see this by replacing sumL and
www.it-ebooks.info
92
sumR with their definitions—if we do so, we still get the same result, but our
program is no longer parallel:
Par.get(Par.unit(sum(l))) + Par.get(Par.unit(sum(r)))
Given what we have decided so far, unit will start evaluating its argument
right away. And the very next thing to happen is that get will wait for that
evaluation to complete. So the two sides of the + sign will not run in parallel if we
simply inline the sumL and sumR variables. Here we can see that unit has a very
definite side-effect, but only with regard to get. That is, unit simply returns a
Par[Int] in this case, representing an asynchronous computation. But as soon as
we pass that Par to get, we explicitly wait for it, exposing the side-effect. So it
seems that we want to avoid calling get, or at least delay calling it until the very
end. We want to be able to combine asynchronous computations without waiting
for them to finish.
Before we continue, notice what we have done. First, we conjured up a simple,
almost trivial example. We next explored this example a bit to uncover a design
choice. Then, via some experimentation, we discovered an interesting consequence
of one option and in the process learned something fundamental about the nature of
our problem domain! The overall design process is a series of these little
adventures. You don't need any special license to do this sort of exploration, and
you don't need to be an expert in functional programming either. Just dive in and
see what you find.
Let's see if we can avoid the above pitfall of combining unit and get. If we
don't call get, that implies that our sum function must return a Par[Int]. What
consequences does this change reveal? Again, let's just invent functions with the
required signatures:
www.it-ebooks.info
93
signature possible (that is, do not assume it works only for Par[Int]).
Observe that we are no longer calling unit in the recursive case, and it isn't
clear whether unit should accept its argument lazily anymore. In this example,
accepting the argument lazily doesn't seem to provide any benefit, but perhaps this
isn't always the case. Let's try coming back to this question in a minute.
What about map2—should it take its arguments lazily? It would make sense for
map2 to run both sides of the computation in parallel, giving each side equal
opportunity to run (it would seem arbitrary for the order of the map2 arguments to
matter—we simply want map2 to indicate that the two computations being
combined are independent, and can be run in parallel). What choice lets us
implement this meaning? As a simple test case, consider what happens if map2 is
strict in both arguments, and we are evaluating sum(IndexedSeq(1,2,3,4))
. Take a minute to work through and understand the following (somewhat stylized)
program trace:
sum(IndexedSeq(1,2,3,4))
map2(
sum(IndexedSeq(1,2)),
sum(IndexedSeq(3,4)))(_ + _)
map2(
map2(
sum(IndexedSeq(1)),
sum(IndexedSeq(2)))(_ + _),
sum(IndexedSeq(3,4)))(_ + _)
map2(
map2(
unit(1),
unit(2))(_ + _),
sum(IndexedSeq(3,4)))(_ + _)
map2(
map2(
unit(1),
unit(2))(_ + _),
map2(
sum(IndexedSeq(3)),
sum(IndexedSeq(4)))(_ + _))(_ + _)
...
www.it-ebooks.info
94
tree of summations first before moving on to (strictly) constructing the right half
(here, sum(IndexedSeq(1,2)) gets fully expanded before we consider
sum(IndexedSeq(3,4))). And if map2 begins evaluating its arguments
immediately (using whatever resource is being used to implement the parallelism,
like a thread pool), that implies the left half of our computation will begin its
execution before we even begin constructing the right half of our computation.
What if we keep map2 strict, but don't have it begin execution immediately?
Does this help? If map2 doesn't begin evaluation immediately, this implies a Par
value is merely constructing a description of what needs to be computed in
parallel. Nothing actually occurs until we evaluate this description, perhaps using a
get-like function. The problem is that if we construct our descriptions strictly,
they are going to be rather heavyweight objects. Looking back at our trace, our
description is going to have to contain the full tree of operations to be performed:
map2(
map2(
unit(1),
unit(2))(_ + _),
map2(
unit(3),
unit(4))(_ + _))(_ + _)
Whatever data structure we use to store this description, it will likely occupy
more space than the original list itself! It would be nice if our descriptions were a
bit more lightweight.
It seems we should make map2 lazy and have it begin immediate execution of
both sides in parallel (this also addresses the problem of giving each side equal
"weight"), but something still doesn't feel right about this. Is it always the case that
we want to evaluate the two arguments to map2 in parallel? Probably not.
Consider this simple hypothetical example:
Par.map2(Par.unit(1), Par.unit(1))(_ + _)
In this case, we happen to know that the two computations we're combining
will execute so quickly that there isn't much point in spawning off a separate
logical thread to evaluate them. But our API doesn't give us any way of providing
this sort of information. That is, our current API is very implicit about when
computations get forked off the main thread—the programmer does not get to
www.it-ebooks.info
95
specify where this forking should occur. What if we make this forking more
explicit? We can do that by inventing another function, def fork[A](a: =>
Par[A]): Par[A], which we can take to mean that the given Par should be
run in a separate logical thread:
With fork, we can now make map2 strict, leaving it up to the programmer to
wrap arguments if they wish. A function like fork solves the problem of
instantiating our parallel computations too strictly (as an exercise, try revisiting the
hypothetical trace from earlier), but more fundamentally it makes the parallelism
more explicit and under programmer control. There are really two separate
concerns being addressed here. The first is that we need some way to indicate that
the results of the two parallel tasks should be combined. Separate from this, we
have the choice of whether a particular task should be performed asynchronously.
By keeping these concerns separate, we avoid having any sort of global policy for
parallelism attached to map2 and other combinators we write, which would mean
making tough (and ultimately arbitrary) choices about what global policy is best.
Such a policy may in practice be inappropriate in many cases.
Let's now return to the question of whether unit should be strict or lazy. With
fork, we can now make unit strict without any loss of expressiveness. A
non-strict version of it, let's call it async, can be implemented using unit and
fork.
www.it-ebooks.info
96
Footnote 4mThis sort of indifference to representation is a hint that the operations are actually more general,
and can be abstracted to work for types other than just Par. We will explore this topic in detail in part 3.
We still have the question of whether fork should begin evaluating its
argument immediately, or wait until the computation is forced later using
something like get. When you are unsure about a meaning to assign to some
function in your API, you can always continue with the design process—at some
point later the tradeoffs of different choices of meaning may become clear. Here,
we make use of a helpful trick—we are going to think about what sort of
information is required to implement fork with various meanings.
If fork begins evaluating its argument immediately in parallel, the
implementation must clearly know something (either directly or indirectly) about
how to create threads or submit tasks to some sort of thread pool. Moreover, if
fork is just a standalone function (as it currently is on Par), it implies that
whatever resource is used to implement the parallelism must be globally accessible
. This means we lose the ability to control the parallelism strategy used for
different parts of our program. And while there's nothing inherently wrong with
having a global resource for executing parallel tasks, we can imagine how it would
be useful to have more fine-grained control over what implementations are used
where (we might like for each subsystem of a large application to get its own
thread pool with different parameters, say).
Notice that coming to these conclusions didn't require knowing exactly how
fork would be implemented, or even what the representation of Par will be. We
just reasoned informally about the sort of information required to actually spawn a
parallel task, and examined the consequences of having Par values know about
this information.
In contrast, if fork simply holds onto the computation until later, this requires
no access to the mechanism for implementing parallelism. Let's tentatively assume
this meaning then for fork. With this model, Par itself does not know how to
actually implement the parallelism. It is more a description of a parallel
computation. This is a big shift from before, where we were considering Par to be
a "container" of a value that we could "get". Now it's more of a first-class program
that we can run. So let's rename our get function to run.
www.it-ebooks.info
97
Because Par now just a pure data structure, we will assume that run has some
means of implementing the parallelism, whether it spawns new threads, delegates
tasks to a thread pool, or uses some other mechanism.
At any point while sketching out an API, you can start thinking about possible
representations for the abstract types that appear.
EXERCISE 2: Before continuing, try to come up with representations for Par
and Strategy that make it possible to implement the functions of our API.
Let's see if we can come up with a representation. We know run needs to
execute asynchronous tasks somehow. We could write our own API, but there's
already a class for this in java.util.concurrent, ExecutorService.
Here it its API, excerpted and transcribed to Scala:
class ExecutorService {
def submit[A](a: Callable[A]): Future[A]
}
trait Future[A] {
def get: A
def get(timeout: Long, unit: TimeUnit): A
def cancel(evenIfRunning: Boolean): Boolean
def isDone: Boolean
def isCancelled: Boolean
}
www.it-ebooks.info
98
Is it really that simple? Let's assume it is for now, and revise our model if we
decide it doesn't allow some functionality we'd like.
www.it-ebooks.info
99
change the rules of our universe at any time, make a fundamental change to our
representation or introduce a new primitive, and explore how our creation then
behaves.
EXERCISE 3: Let's begin by implementing the functions of the API we've
developed so far. Now that we have a representation for Par, we should be able to
fill these in. Optional (hard): try to ensure your implementations respect the
contract of the get method on Future that accepts a timeout.
You can place these functions and other functions we write inside an object
called Par, like so:
object Par {
/* Functions go here */
}
What else can we express with our existing combinators? Let's look at a more
concrete example.
Suppose we have a Par[List[Int]] representing a parallel computation
www.it-ebooks.info
100
We could of course run the Par, sort the resulting list, and re-package it in a
Par with unit. But we want to avoid calling run. The only other combinator we
have that allows us to manipulate the value of a Par in any way is map2. So if we
passed l to one side of map2, we would be able to gain access to the List inside
and sort it. And we can pass whatever we want to the other side of map2, so let's
just pass a no-op:
Nice. We can now tell a Par[List[Int]] that we would like that list sorted.
But we can easily generalize this further. We can "lift" any function of type A =>
B to become a function that takes Par[A] and returns Par[B]. That is, we can
map any function over a Par:
This was almost too easy. We just combined the operations to make the types
line up. And yet, if you look at the implementations of map2 and unit, it should
be clear this implementation of map means something sensible.
Was it cheating to pass a bogus value, unit(()) as an argument to map2,
only to ignore its value? Not at all! It shows that map2 is strictly more powerful
than map. This sort of thing can be a hint that map2 can be further decomposed
into primitive operations. And when we consider it, map2 is actually doing two
things—it is creating a parallel computation that waits for the result of two other
www.it-ebooks.info
101
computations and then it is combining their results using some function. We could
split this into two functions, product and map:
www.it-ebooks.info
102
we've defined. Knowledge of what combinators are truly primitive will become
more important in Part 3, when we learn to abstract over common patterns across
libraries.5
Footnote 5mIn this case, there's another good reason not to implement parMap as a new primitive—it's
challenging to do correctly, particularly if we want to properly respect timeouts. It's frequently the case that
primitive combinators encapsulate some rather tricky logic, and reusing them means we don't have to duplicate this
logic.
Let's see how far we can get implementing parMap in terms of existing
combinators:
Notice that we've wrapped our implementation in a call to fork. With this
implementation, parMap will return immediately, even for a huge input list. When
we later call run, it will fork a single asynchronous computation which itself
spawns N parallel computations then waits for these computations to finish,
collecting their results up into a list. If you look back at your previous
www.it-ebooks.info
103
Can you think of any other useful functions to write? Experiment with writing a
few parallel computations of your own to see which ones can be expressed without
additional primitives. Here are some ideas to try:
Is there a more general version of the parallel summation function we wrote at the
beginning of this chapter? Try using it to find the maximum value of an IndexedSeq in
parallel.
Write a function that takes a list of paragraphs (a List[String]), and returns the total
number of words across all paragraphs, in parallel. Generalize this function as much as
possible.
Implement map3, map4, and map5, in terms of map2.
Up until now, we have been reasoning somewhat informally about our API.
There's nothing wrong with this, but it can be helpful to take a step back and
formalize what laws you expect to hold (or would like to hold) for your API.7
www.it-ebooks.info
104
Without realizing it, you have probably mentally built up a model of what
properties or laws you expect. Actually writing these down and making them
precise can highlight design choices that wouldn't be otherwise apparent when
reasoning informally.
Footnote 7mWe'll have much more to say about this throughout the rest of this book. In the next chapter, we'll
be designing a declarative testing library that lets us define properties we expect functions to satisfy, and
automatically generates test cases to check these properties. And in Part 3 we'll introduce abstract interfaces
specified only by sets of laws.
Like any design choice, choosing laws has consequences—it places constraints
on what the operations can mean, what implementation choices are possible,
affects what other properties can be true or false, and so on. Let's look at an
example. We are going to simply conjure up a possible law that seems reasonable.
This might be used as a test case if we were creating unit tests for our library:
map(unit(1))(_ + 1) == unit(2)
map(unit(x))(f) == unit(f(x))
Here we are saying this should hold for any choice of x and f. This places
www.it-ebooks.info
105
Footnote 11mThis is the same sort of substitution and simplification one might do when solving an algebraic
equation.
map(unit(x))(f) == unit(f(x))
map(unit(x))(id) == unit(id(x))
map(unit(x))(id) == unit(x)
map(y)(id) == y
Fascinating! Our new, simpler law talks only about map—apparently the
mention of unit was an extraneous detail. To get some insight into what this new
law is saying, let's think about what map cannot do. It cannot, say, throw an
exception and crash the computation before applying the function to the result (can
you see why this violates the law?). All it can do is apply the function f to the
result of y, which of course, leaves y unaffected in the case that function is id. 12
Even more interesting, given map(y)(id) == y, we can perform the
substitutions in the other direction to get back our original, more complex law.
www.it-ebooks.info
106
(Try it!) Logically, we have the freedom to do so because map cannot possibly
behave differently for different function types it receives. Thus, given
map(y)(id) == y, it must be true that map(unit(x))(f) ==
unit(f(x)). Since we get this second law or theorem "for free", simply because
of the parametricity of map, it is sometimes called a free theorem.13
Footnote 12mWe say that map is required to be structure-preserving, in that it does not alter the structure of
the parallel computation, only the value "inside" the computation.
Footnote 13mThe idea of free theorems was introduced by Philip Wadler in a classic paper called Theorems
for free!
As interesting as all this is, these laws don't do much to constrain our
implementation. You have probably been assuming these properties without even
realizing it (it would be rather strange to have any special cases in the
implementations of map, unit or ExecutorService.submit, or have map
randomly throwing exceptions). Let's consider a stronger property, namely that
fork should not affect the result of a parallel computation:
fork(x) == x
www.it-ebooks.info
107
implementer hat, put on your debugging hat, and try to break your law. Think
through any possible corner cases, try to come up with counterexamples, and even
construct an informal proof that the law holds—at least enough to convince a
skeptical fellow programmer.
Let's try this mode of thinking. We are expecting that fork(x) == x for all
choices of x, and any choice of ExecutorService. We have a pretty good
sense of what x could be—it's some expression making use of fork, unit, and
map2 (and other combinators derived from these). What about
ExecutorService? What are some possible implementations of it? There's a
good listing of different implementations in the class Executors (API link).
EXERCISE 10 (hard, optional): Take a look through the various static methods
in Executors to get a feel for the different implementations of
ExecutorService that exist. Then, before continuing, go back and revisit your
implementation of fork and try to find a counterexample or convince yourself
that the law holds for your implementation.
www.it-ebooks.info
108
val a = async(42 + 1)
val S = Executors.newFixedSizeThreadPool(1)
println(Par.equal(S)(a, fork(a)))
Most implementations of fork will result in this code deadlocking. Can you
see why? Most likely, your implementation of fork looks something like this:16
Footnote 16mThere's actually another minor problem with this implementation—we are just calling get on
the inner Future returned from fa. This means we are not properly respecting any timeouts that have been
placed on the outer Future.
The bug is somewhat subtle. Notice that we are submitting the Callable
first, and within that callable, we are submitting another Callable to the
ExecutorService and blocking on its result (recall that a(es) will submit a
Callable to the ExecutorService and get back a Future). This is a
problem if our thread pool has size 1. The outer Callable gets submitted and
picked up by the sole thread. Within that thread, before it will complete, we submit
and block waiting for the result of another Callable. But there are no threads
available to run this Callable. Our code therefore deadlocks.
EXERCISE 11 (hard, optional): Can you show that any fixed size thread pool
can be made to deadlock given this implementation of fork?
When you find counterexamples like this, you have two choices—you can try
to fix your implementation such that the law holds, or you can refine your law a
bit, to state more explicitly the conditions under which it holds (we could simply
www.it-ebooks.info
109
stipulate that we require thread pools that can grow unbounded). Even this is a
good exercise—it forces you to document invariants or assumptions that were
previously implicit.
We are going to try to fix our implementation, since being able to run our
parallel computations on fixed size thread pools seems like a useful capability. The
problem with the above implementation of fork is that we are invoking submit
inside a callable, and we are blocking on the result of what we've submitted. This
leads to deadlock when there aren't any remaining threads to run the task we are
submitting. So it seems we have a simple rule we can follow to avoid deadlock:
A Callable should never submit and then block on the result of a
Callable.
You may want to take a minute to prove to yourself that our parallel tasks
cannot deadlock, even with a fixed-size thread pool, so long as this rule is
followed.
Let's look at a different implementation of fork:
This certainly avoids deadlock. The only problem is that we aren't actually
forking a separate logical thread to evaluate fa . So,
fork(hugeComputation)(es) for some ExecutorStrategy, es, runs
hugeComputation in the main thread, which is exactly what we wanted to
avoid by calling fork. This is still a useful combinator, though, since it lets us
delay instantiation of a parallel computation until it is actually needed. Let's give it
a name, delay:
EXERCISE 12 (hard, optional): Can you figure out a way to still evaluate fa in
a separate logical thread, but avoid deadlock? You may have to resort to some
mutation or imperative tricks behind the scenes. There's absolutely nothing wrong
with doing this, so long as these local violations of referential transparency aren't
www.it-ebooks.info
110
observable to users of the API. The details of this are quite finicky to get right. The
nice thing is that we are confining this detail to one small block of code, rather than
forcing users to have to think about these issues throughout their use of the API.
Taking a step back from these details, the purpose here is not necessarily to
figure out the best, nonblocking implementation of fork, but more to show that
laws are important. They give us another angle to consider when thinking about the
design of a library. If we hadn't tried writing some of these laws out, we may not
have discovered this behavior of fork until much later.
In general, there are multiple approaches you can consider when choosing laws
for your API. You can think about your conceptual model, and reason from there to
postulate laws that should hold. You can also conjure up laws you think are useful
for reasoning or compositionality (like we did with our fork law), and see if it is
possible and sensible to ensure they hold for your model and implementation. And
lastly, you can look at your implementation and come up with laws you expect to
hold based on your implementation.17
Footnote 17mThis last way of generating laws is probably the weakest, since it can be a little too easy to just
have the laws reflect the implementation, even if the implementation is buggy or requires all sorts of unusual side
conditions that make composition difficult.
EXERCISE 13: Can you think of other laws that should hold for your
implementation of unit, fork, and map2? Do any of them have interesting
consequences?
www.it-ebooks.info
111
Notice what happens when we try to define this using only map. If we try
map(a)(a => if (a) ifTrue else ifFalse), we obtain the type
Par[Par[A]]. If we had an ExecutorStrategy we could force the outer
Par to obtain a Par[A], but we'd like to keep our Par values agnostic to the
ExecutorStrategy. That is, we don't want to actually execute our parallel
computation, we simply want to describe a parallel computation that first runs one
parallel computation and then uses the result of that computation to choose what
computation to run next.
This is a case where our existing primitives are insufficient. When you
encounter these situations, you can respond by simply introducing the exact
combinator needed (for instance, we could simply write choice as a new
primitive, using the fact that Par[A] is merely an alias for ExecutorService
=> Future[A] rather than relying only on unit, fork, and map2). But quite
often, the example that motivates the need for a new primitive will not be minimal
—it will have some incidental features that aren't really relevant to the essence of
the API's limitation. It's a good idea to try to explore some related examples around
the particular one that cannot be expressed, to see if a common pattern emerges.
Let's do that here.
If it's useful to be able to choose between two parallel computations based on
the results of a first, it should be useful to choose between N computations:
Let's say that choiceN runs a, then uses that to select a parallel computation
from choices. This is a bit more general than choice.
EXERCISE 15: Implement choiceN and then choice in terms of choiceN
.
EXERCISE 16: Still, let's keep looking at some variations. Try implementing
the following combinator. Here, instead of a list of computations, we have a Map
of them:18
Footnote 18mMap (API link) is a purely functional data structure.
If you want, stop reading here and see if you can come up with a combinator
www.it-ebooks.info
112
Is flatMap really the most primitive possible function? Let's play around with
it a bit more. Recall when we first tried to implement choice, we ended up with a
Par[Par[A]]. From there we took a step back, tried some related examples, and
eventually discovered flatMap. But suppose instead we simply conjured another
combinator, let's call it join, for converting Par[Par[A]] to Par[A]:
www.it-ebooks.info
113
Can you implement a function with the same signature as map2, but using bind and unit?
How is its meaning different than that of map2?
Can you think of laws relating join to the other primitives of the algebra?
Are there parallel computations that cannot be expressed using this algebra? Can you
think of any computations that cannot even be expressed by adding new primitives to the
algebra?
In this chapter, we've chosen a "pull" model for our parallel computations. That is, when
we run a computation, we get back a Future, and we can block to obtain the result of this
Future. Are there alternative models for Par that don't require us to ever block on a
Future?
7.3 Conclusion
In this chapter, we worked through the design of a library for defining parallel and
asynchronous computations. Although this domain is interesting, the goal of this
chapter was to give you a window into the process of functional design, to give you
a sense of the sorts of issues you're likely to encounter and to give you ideas for
how you can handle them. If you didn't follow absolutely every part of this, or if
certain conclusions felt like logical leaps, don't worry. No two people take the
same path when designing a library, and as you get more practice with functional
design, you'll start to develop your own tricks and techniques for exploring a
problem and possible designs.
In the next chapter, we are going to look at a completely different domain, and
take yet another meandering journey toward discovering an API for that domain.
www.it-ebooks.info
114
Index Terms
free theorem
map fusion
parametricity
shape
structure-preserving
typecasing
www.it-ebooks.info
115
8.1 Introduction
Property-based testing
8
In the last chapter, we worked through the design of a functional library for
expressing parallel computations. There, we introduced the idea of an API forming
an algebra—that is, a collection of data types, functions over these data types, and
importantly, laws or properties that express relationships between these functions.
We also hinted at the idea that it might be possible to somehow check these laws
automatically.
This chapter will work up to the design and implementation of a simple but
powerful property-based testing library. What does this mean? The general idea of
such a library is to decouple the specification of program behavior from the
creation of test cases. The programmer focuses on specifying the behavior and
giving high-level constraints on the test cases; the framework then handles
generating (often random) test cases satisfying the constraints and checking that
programs behave as specified for each case.
www.it-ebooks.info
116
Check that reversing a list twice gives back the original list
Check that the first element becomes the last element after reversal
A property which is obviously false
scala> prop.check
+ OK, passed 100 tests.
scala> failingProp.check
! Falsified after 6 passed tests.
> ARG_0: List("0", "1")
www.it-ebooks.info
117
Reversing a list and summing it should give the same result as summing
the original, non-reversed list.
What should the sum be if all elements of the list are the same value?
Test case minimization: In the event of discovering a failing test, the test runner tries
smaller sizes until finding the minimal size test case that also fails, which is more
illuminating for debugging purposes. For instance, if a property fails for a list of size 10,
the test runner checks smaller lists and reports the smallest failure.
Exhaustive test case generation: We call the set of possible values that could be produced
by some Gen[A] the domain.2 When the domain is small enough (for instance, suppose
the domain were all even integers less than 100), we may exhaustively generate all its
values, rather than just randomly from it. If the property holds for all values in a domain,
we have an actual proof, rather than just the absence of evidence to the contrary.
Footnote 2mThis is the same usage of 'domain' as the domain of a function—generators describe
possible inputs to functions we would like to test. Note that we will also still sometimes use 'domain' in the
more colloquial sense, to refer to a subject or area of interest, e.g. "the domain of functional parallelism" or
"the error-handling domain".
ScalaCheck is just one property-based testing library. And while there's nothing
wrong with it, we are going to be deriving our own library in this chapter, starting
from scratch. This is partially for pedagogical purposes, but there's another reason:
we want to encourage the view that no existing library (even one designed by
supposed experts) is authoritative. Don't treat existing libraries as a cookbook to be
followed. Most libraries contain a whole lot of arbitrary design choices, many
made unintentionally. Look back to the previous chapter—notice how on several
occasions, we did some informal reasoning to rule out entire classes of possible
designs. This sort of thing is an inevitable part of the design process (it is
impossible to fully explore every conceivable path), but it means it's easy to miss
out on workable designs.
When you start from scratch, you get to revisit all the fundamental assumptions
that went into designing the library, take a different path, and discover things about
www.it-ebooks.info
118
the domain and the problem space that others may not have even considered. As a
result, you might arrive at a design that's much better for your purposes. But even
if you decide you like the existing library's solution, spending an hour or two of
playing with designs and writing down some type signatures is a great way to learn
more about a domain, understand the design tradeoffs, and improve your ability to
think through design problems.
www.it-ebooks.info
119
since it doesn't seem like Gen.listOf should care about the type of the Gen it
receives as input (it would be odd to require separate combinators for creating lists
of Int, Double, String, and so on), let's go ahead and make it polymorphic:
We can learn many things by looking at this signature. Notice what we are not
specifying—the size of the list to generate. For this to be implementable, this
implies our generator must either assume or be told this size. Assuming a size
seems a bit inflexible—whatever we assume is unlikely to be appropriate in all
contexts. So it seems that generators must be told the size of test cases to generate.
We could certainly imagine an API where this were explicit:
Here, we've simply invented a new type, Prop (short for "property", following
the ScalaCheck naming), for the result of binding a Gen to a predicate. We might
not know the internal representation of Prop or what other functions it supports
but based on this example, we can see that it has an && operator, so let's introduce
that:
www.it-ebooks.info
120
trait Prop {
def check: Unit
def &&(p: Prop): Prop = ???
}
In order to combine Prop values using combinators like &&, we need check
(or whatever function "runs" properties) to return some meaningful value. What
type should that value be? Well, let's consider what sort of information we'd like to
get out of checking our properties. At a minimum, we need to know whether the
property succeeded or failed. This lets us implement &&.
EXERCISE 3: Assuming the following definition of Prop, implement && as a
method of Prop:
object Prop {
www.it-ebooks.info
121
There's a problem, what type do we return in the failure case? We don't know
anything about the type of the test cases being generated internal to the Prop .
Should we add a type parameter to Prop, make it a Prop[A]? Then check
could return Either[A,Int]. Before going too far down this path, let's ask
ourselves, do we really care though about the type of the value that caused the
property to fail? Not exactly. We would only care about the type if we were going
to do further computation with this value. Most likely we are just going to end up
printing this value to the screen, for inspection by the person running these tests.
After all, the goal here is a) to find bugs, and b) to indicate to a person what test
cases trigger those bugs, so they can go fix them. This suggests we can get away
with the following type:
object Prop {
type FailedCase = String
type SuccessCount = Int
}
www.it-ebooks.info
122
by inspecting the way Prop values are created. In particular, let's look at forAll:
Without knowing more about the representation of Gen, it's hard to say whether
there's enough information here to be able to generate values of type A (which is
what we need to implement check). So for now let's turn our attention to Gen, to
get a better idea of what it means and what its dependencies might be.
www.it-ebooks.info
123
The first element of the pair is the generator of random values, the second is an
exhaustive list of values. Note that with this representation, the test runner will
likely have to choose between the two modes based on the number of test cases it
is running. For instance, if the test runner is running 1000 tests, it could 'spend' up
to the first 300 of these tests working through the domain exhaustively, then switch
to randomly sampling the domain if the domain has not been fully enumerated.
We'll get to writing this logic a bit later, after we nail down exactly how to
represent our "dual-mode" generators.
www.it-ebooks.info
124
So far so good. But these domains are all finite. What should we do about
infinite domains, like a Double generator in some range:
www.it-ebooks.info
125
do for exhaustive? Return the empty Stream? No, probably not. Previously,
we made a choice about the meaning of an empty stream—we interpreted it to
mean that we have finished exhaustively generating values in our domain and there
are no more values to generate. We could change its meaning to "the domain is
infinite, use random sampling to generate test cases", but then we lose the ability to
determine that we have exhaustively enumerated our domain, or that the domain is
simply empty. How can we distinguish these cases? One simple way to do this is
with Option:
We'll adopt the convention that a None in exhaustive signals to the test
runner should switch to random sampling, because the domain is infinite or
otherwise not worth fully enumerating.6 If the domain can be fully enumerated,
exhaustive will be a finite stream of Some values. Note that this is a pretty
typical usage of Option. Although we introduced Option as a way of doing
error handling, Option gets used a lot whenever we need a simple way of
encoding one of two possible cases.
Footnote 6mWe could also choose to put the Option on the outside: Option[Stream[A]]. You may
want to explore this representation on your own. You will find that it doesn't work out so well as it requires that we
be able to decide up front that the domain is not worth enumerating. We will see examples later of generators
where it isn't possible to make this determination.
www.it-ebooks.info
126
If we can generate a single Int in some range, do we need a new primitive to generate an
(Int,Int) pair in some range?
Can we produce a Gen[Option[A]] from a Gen[A]? What about a Gen[A] from a
Gen[Option[A]]?
www.it-ebooks.info
127
Here, we are going to take a bit of a shortcut. Notice that Gen is composed of a
few other types, Stream, State, and Option. This can often be a hint that the
API of Gen is going to have many of the same operations as these types. Let's see
if there's some familiar operations from Stream, State, and Option that we
can also define for Gen.
EXERCISE 7: Aha! Our Gen data type supports both map and map2.7 See if
you can implement these. Your implementation should be almost trivially defined
in terms of the map and map2 functions on Stream, State, and Option.8 You
can add them as methods of the Gen type, as we've done before, or write them as
standalone functions in the Gen companion object. After you've implemented map
, you may want to revisit your implementation of choose for Double and define
it in terms of uniform and map.
Footnote 7mYou've probably noticed by now that many data types support map, map2, and flatMap. We'll
be discussing how to abstract over these similarities in part 3 of the book.
Footnote 8mIn part 3 we will also learn how to derive these implementations automatically. That is, by
composing our data types in certain well-defined ways, we can obtain the implementations of map, map2, and
so on for free, without having to write any additional code!
www.it-ebooks.info
128
So far so good. But map and map2 are not expressive enough to encode some
generators. Suppose we'd like a Gen[(Int,Int)] where both integers are odd,
or both are even. Or a Gen[List[Int]] where we first generate a length
between 0 and 11, then generate a List[Double] of the chosen length. In both
these cases there is a dependency—we generate a value, then use that value to
determine what generator to use next.9 For this we need flatMap, another
function we've seen before.
Footnote 9mTechnically, this first case can be implemented by generating the two integers separately, and
using map2 to make them both odd or both even. But a more natural way is to choose an even or odd
generator based on the first value generated.
www.it-ebooks.info
129
It only includes two cases, one for success, and one for failure. We can create a
new data type, Status, to represent the two ways a test can succeed:
trait Status
case object Proven extends Status
case object Unfalsified extends Status
A test can succeed by being proven, if the domain has been fully enumerated
and no counterexamples found, or it can be merely unfalsified, if the test runner
had to resort to random sampling.
Prop is now nothing more than a non-strict Either. But Prop is still
missing some information—we have not specified how many test cases to examine
before we consider the property to be passed. We could certainly hardcode
something, but it would be better to propagate this dependency out:
www.it-ebooks.info
130
We can see that forAll does not have enough information to return a Prop.
Besides the number of test cases to run, Prop must have all the information
needed for generated test cases to return a Status, but if it needs to generate
random test cases, it is going to need an RNG. Let's go ahead and propagate that
dependency to Prop:
We can start to see a pattern here. There are certain parameters that go into
generating test cases, and if we think of other parameters besides the number of
test cases and the source of randomness, we can just add these as extra arguments
to Prop.
We now have enough information to actually implement forAll. Here is a
simple implementation. We have a choice about whether to use exhaustive or
random test case generation. For our implementation, we'll spend a third of our test
cases examining elements from the exhaustive Stream. If we reach the end of that
Stream or find a counterexample, we return immediately, otherwise, we fall back
to generating random test cases for our remaining test cases:10
Footnote 10mHere is a question to explore—might there be a way to track the expected size of the exhaustive
stream, such that the decision to use random data could be made up front? For some primitives, it is certainly
possible, but is it possible for all our primitives?
www.it-ebooks.info
131
else Left(h.toString) }
catch { case e: Exception => Left(buildMsg(h, e)) }
case Some((None,_)) => Right((Unfalsified,i))
case None => onEnd(i)
}
go(0, n/3, a.exhaustive, i => Right((Proven, i))) match {
case Right((Unfalsified,_)) =>
val rands = randomStream(a)(rng).map(Some(_))
go(n/3, n, rands, i => Right((Unfalsified, i)))
case s => s
}
}
}
Notice we are catching exceptions and reporting them as test failures, rather
than bringing down the test runner (which would lose information about what
argument triggered the failure).
EXERCISE 12: Now that we have a representation of Prop, implement &&,
and || for manipulating Prop values. While we can implement &&, notice that in
the case of failure we aren't informed which property was responsible, the left or
the right. (Optional): Can you devise a way of handling this, perhaps by allowing
Prop values to be assigned a tag or label which gets displayed in the event of a
failure?
www.it-ebooks.info
132
Shrinking: After we have found a failing test case, we can run a separate procedure to
minimize the test case, by successively decreasing its "size" until it no longer fails. This
is called shrinking, and it usually requires us to write separate code for each data type to
implement this minimization process.
Sized generation: Rather than shrinking test cases after the fact, we simply generate our
test cases in order of increasing size and complexity. So, we start small and increase size
until finding a failure. This idea can be extended in various ways, to allow the test runner
to make larger jumps in the space of possible sizes while still making it possible to find
the smallest failing test.
EXERCISE 13: Implement helper functions for converting Gen to SGen. You
can add this as a method to Gen.
EXERCISE 15: We can now implement a listOf combinator that does not
accept an explicit size. It can return an SGen instead of a Gen . The
www.it-ebooks.info
133
Let's see how SGen affects the definition of Prop and Prop.forAll. The
SGen version of forAll looks like this:
Can you see how this function is not possible to implement? SGen is expecting
to be told a size, but Prop does not receive any size information. Much like we
did with the source of randomness and number of test cases, we simply need to
propagate this dependency to Prop. But rather than just propagating this
dependency as is to the caller of Prop, we are going to have Prop accept a
maximum size. This puts Prop in charge of invoking the underlying generators
with various sizes, up to and including the maximum specified size, which means it
can also search for the smallest failing test case. Let's see how this works out:12
Footnote 12mThis rather simplistic implementation gives an equal number of test cases to each size being
generated, and increases the size by 1 starting from 0. We could imagine a more sophisticated implementation
that does something more like a binary search for a failing test case size—starting with sizes
0,1,2,4,8,16... then narrowing in on smaller sizes in the event of a failure.
www.it-ebooks.info
134
examined (for instance, a SGen[Boolean] can only ever generate two distinct
values, regardless of size). It can be exhausted, if the domain of the generator has
been fully examined, but only up through the maximum size. Or it could be merely
unfalsified, if we had to resort to random generation and no counterexamples were
found. Let's add this to our Status representation:
trait SGen[+A]
case class Sized[+A](forSize: Size => Gen[A]) extends SGen[A]
case class Unsized[+A](get: Gen[A]) extends SGen[A]
EXERCISE 17: Implement forAll for this representation of SGen and any
other functions you've implemented that require updating to reflect these changes
in representation. Notice that we only need to update primitive
combinators—derived combinators get their new behavior "for free", based on the
updated implementation of primitives.
8.3.4 Using the library, improving its usability, and future directions
We have converged on what seems like a reasonable API. We could keep tinkering
with it, but at this point let's try using the library to construct tests and see if we
notice any deficiencies, either in what it can express or its general usability.
Usability is somewhat subjective, but we generally like to provide convenient
syntax and appropriate helper functions which abstract out common patterns that
occur in client usage of the library. We aren't necessarily aiming here to make the
library more expressive, we simply want to make it more pleasant to use.
Let's revisit an example that we mentioned at the start of this
www.it-ebooks.info
135
We can introduce a helper function in Prop for actually running our Prop
values and printing their result to the console in a useful format:
We are using default arguments here to make it more convenient to call in the
case that the defaults are fine.
EXERCISE 18: Try running Prop.run(maxProp). Notice that it fails!
Property-based testing has a way of revealing all sorts of hidden assumptions
we have about our code, and forcing us to be much more explicit about these
assumptions. The Scala standard library implementation of max crashes when
given the empty list (rather than returning an Option).
EXERCISE 19: Define listOf1, for generating nonempty lists, then update
your specification of max to use this generator.
Let's try a few more examples.
www.it-ebooks.info
136
Recall that in the previous chapter we looked at laws we expected to hold for
our parallel computations. Can we express these laws with our library? The first
"law" we looked at was actually a particular test case:
map(unit(1))(_ + 1) == unit(2)
We've expressed the test, but it's verbose, cluttered, and the "idea" of the test is
obscured by details that aren't really relevant here. Notice that this isn't a question
of the API being expressive enough—yes we can express what we want, but a
combination of missing helper functions and poor syntax obscures the intent.
Let's improve on this. Our first observation is that forAll is a bit too general
for this test case. We aren't varying the input to this test, we just have a hardcoded
example. Hardcoded examples should be just as convenient to write as in a
traditional unit testing library. Let's introduce a combinator for it:
www.it-ebooks.info
137
Is this cheating? Not at all. We provide the unit generator, which, of course,
only generates a single value. The value will be ignored in this case, simply used to
drive the evaluation of the given Boolean. Notice that this combinator is
general-purpose, having nothing to do with Par—we can go ahead and move it
into the Prop companion object. Updating our test case to use it gives us:
val p2 = check {
val p = Par.map(Par.unit(1))(_ + 1)
val p2 = Par.unit(2)
p(ES).get == p2(ES).get
}
val p3 = check {
equal (
Par.map(Par.unit(1))(_ + 1),
Par.unit(2)
) (ES) get
}
So, we are lifting equality to operate in Par, which is a bit nicer than having to
run each side separately. But while we're at it, why don't we move the running of
Par out into a separate function, forAllPar, and the analogous checkPar.
This also gives us a good place to insert variation across different parallel
strategies, without it cluttering up the property we are specifying:
val S = weighted(
choose(1,4).map(Executors.newFixedThreadPool) -> .75,
unit(Executors.newCachedThreadPool) -> .25)
www.it-ebooks.info
138
Much nicer:
object ** {
def unapply[A,B](p: (A,B)) = Some(p)
}
www.it-ebooks.info
139
Footnote 16mWe cannot use the standard Java/Scala equals method, or the == method in Scala (which
delegates to the equals method), since that method returns a Boolean directly, and we need to return a
Par[Boolean]. Some infix syntax for equal might be nice. See the chapter code for the previous chapter
on purely functional parallelism for an example of how to do this.
val p2 = checkPar {
equal (
Par.map(Par.unit(1))(_ + 1),
Par.unit(2)
)
}
These might seem like minor changes, but this sort of factoring and cleanup can
greatly improve the usability of your library, and the helper functions we've written
make the properties easier to read and more pleasant to write. You may want to add
versions of forAllPar and checkPar for sized generators as well.
Let's look at some other properties from the previous chapter. Recall that we
generalized our test case:
map(unit(x))(f) == unit(f(x))
map(y)(id) == y
Can we express this? Not exactly. This property implicitly states that the
equality holds for all choices of y, for all types. We are forced to pick particular
values for y:
We can certainly range over more choices of y, but what we have here is
probably good enough. The implementation of map cannot care about the values of
our parallel computation, so there isn't much point in constructing the same test for
Double, String, and so on. What map can be affected by is the structure of the
parallel computation. If we wanted greater assurance that our property held, we
www.it-ebooks.info
140
could provide richer generators for the structure. Here, we are only supplying Par
expressions with one level of nesting.
EXERCISE 21 (hard): Writer a richer generator for Par[Int], which builds
more deeply nested parallel computations than the simple ones we gave above.
EXERCISE 22: Express the property about fork from last chapter, that
fork(x) == x.
So far, our library seems quite expressive, but there's one area where it's
lacking: we don't currently have a good way to test higher-order functions. While
we have lots of ways of generating data, using our generators, we don't really have
a good way of generating functions.
For instance, let's consider the takeWhile function defined for List and
Stream. Recall that this function returns the longest prefix of its input whose
elements all satisfy a predicate. For instance, List(1,2,3).takeWhile(_ <
3) results in List(1,2). A simple property we'd like to check is that for any
stream, s: List[A] , and any f: A => Boolean ,
s.takeWhile(f).forall(f), that is, every element in the returned stream
satisfies the predicate.17
Footnote 17mIn the Scala standard library, forall is a method on List and Stream with the signature
def forall[A](f: A => Boolean): Boolean.
EXERCISE 23: Come up with some other properties that takeWhile should
satisfy. Can you think of a good property expressing the relationship between
takeWhile and dropWhile?
We could certainly take the approach of only examining particular arguments
when testing HOFs like takeWhile. For instance, here's a more specific property
for takeWhile:
This works, but is there a way we could let the testing framework handle
generating functions to use with takeWhile?18 Let's consider our options. To
make this concrete, let's suppose we have a Gen[Int] and would like to produce
www.it-ebooks.info
141
a Gen[String => Int]. What are some ways we could do that? Well, we
could produce String => Int functions that simply ignore their input string
and delegate to the underlying Gen[Int].
Footnote 18mRecall that in the previous chapter we introduced the idea of free theorems and discussed how
parametricity frees us somewhat from having to inspect the behavior of a function for every possible type argument.
Still, there are many situations where being able to generate functions for testing is useful.
Try writing properties to specify the behavior of some of the other functions we wrote for
List and Stream, for instance take, drop, filter, and unfold.
Try writing a sized generator for producing the Tree data type we defined in chapter 3,
then use this to specify the behavior of the fold function we defined for Tree. Can you
think of ways to improve the API to make this easier?
Try writing properties to specify the behavior of the sequence function we defined for
Option and Either.
www.it-ebooks.info
142
And in this chapter we defined map for Gen (as a method on Gen[A]):
And we've defined very similar-looking functions for other data types. We have
to wonder, is it merely that our functions share similar-looking signatures, or do
they satisfy the same laws as well? Let's look at a law we introduced for Par in the
previous chapter:
map(x)(id) == x
EXERCISE 27: Does this law hold for our implementation of Gen.map? What
about for Stream, List, Option and State?
Fascinating! Not only do these functions share similar-looking signatures, they
also in some sense have analogous meanings in their respective domains.
EXERCISE 28 (hard, optional): Spend a little while thinking up laws for some
of the functions with similar signatures you've written for List , Option, and
State. For each law, see if an analogous law holds for Gen.
It appears there are deeper forces at work! We are uncovering some rather
fundamental patterns that cut across domains. In part 3, we'll learn the names for
these patterns, discover the laws that govern them, and understand what it all
means.19
Footnote 19mIf curiosity is really getting the better of you, feel free to peek ahead at Part 3.
www.it-ebooks.info
143
8.4 Conclusion
In this chapter, we worked through another extended exercise in functional library
design, using the domain of property-based testing as inspiration. Once again, we
reiterate that our goal was not necessarily to learn about property-based testing per
se, but to give a window into the process of functional design. We hope these
chapters are giving you ideas about how to approach functional library design in
your own way and preparing you for the sorts of issues and questions you'll
encounter. Developing an understanding of the overall process is much more
important than following absolutely every small design decision we made as we
explored the space of this particular domain.
In the next chapter, we'll look at another domain, parsing, with its own set of
challenges and design questions. As we'll see, similar patterns will emerge.
Index Terms
lifting
primitive vs. derived operations
shrinking, test cases
test case minimization
test case minimization
www.it-ebooks.info
144
9.1 Introduction
Parser combinators
9
In this chapter, we will work through the design of a combinator library for
creating parsers, using JSON parsing as a motivating use case. As in the past two
chapters, we will use this opportunity to provide insight into the process of
functional design and notice common patterns that we'll discuss more in part 3.
This chapter will introduce a design approach called algebraic design. This is
just a natural evolution of what we've already been doing to different degrees in
past chapters—designing our interface and associated laws first and letting this
guide our choice of data type representations. Here, we take this idea to its logical
limit to see what it buys us.
At a few key points during this chapter, we will be giving more open-ended
exercises, intended to mimic the scenarios you might encounter when designing
and implementing your own libraries from scratch. You'll get the most out of this
chapter if you use these opportunities to put the book down and spend some time
investigating possible approaches. When you design your own libraries, you won't
be handed a nicely chosen sequence of type signatures to fill in with
implementations. You will have to make the decisions about what types and
combinators should even exist and a goal in part 2 of this book has been to prepare
you for doing this on your own. As always, in this chapter, if you get stuck on one
of the exercises or want some more ideas, you can keep reading or consult the
answers.
www.it-ebooks.info
145
There are a lot of different parsing libraries.3 Ours will be designed for
expressiveness (we'd like to be able to parse arbitrary grammars), speed, and good
error reporting. This last point is important—if there are parse errors, we want to
be able to indicate exactly where the error is and accurately indicate its cause.
www.it-ebooks.info
146
OK, let's begin. For simplicity and for speed, our library will create parsers that
operate on strings as input.4 We need to pick some parsing tasks to help us
discover a good algebra for our parsers. What should we look at first? Something
practical like parsing an email address, JSON, or HTML? No! These can come
later. For now we are content to focus on a pure, simple domain of parsing various
combinations of repeated letters and jibberish words like "abracadabra" and
"abba". As silly as this sounds, we've seen before how simple examples like this
help us ignore extraneous details and focus on the essence of the problem.
Footnote 4mThis is certainly a simplifying design choice. We can make the parsing library more generic, at
some cost. See the chapter notes for more discussion.
So let's start with the simplest of parsers, one that recognizes the single
character input "a". As we've done in past chapters, we can just invent a
combinator for the task, char:
Wait a minute, what is ParseError? It's another type we've just conjured
www.it-ebooks.info
147
What's with the funny Parser[_] type argument? It's not too important for
right now, but that is Scala's syntax for a type parameter that is itself a type
constructor.5 Just like making ParseError a type argument lets the Parsers
interface work for multiple representations of ParseError, making
Parser[_] a type parameter means the interface works for multiple
representations of Parser, which itself can be applied to one type argument.6
This code will compile as is, without us having to pick a representation for
ParseError or Parser, and you can continue placing additional combinators
in the body of this trait.
Footnote 5mWe will say much more about this in the next chapter. We can indicate that the Parser[_] type
parameter should be covariant in its argument with the syntax Parser[+_].
Footnote 6mWe will say much more about this in the next chapter. We can indicate that the Parser[_] type
parameter should be covariant in its argument with the syntax Parser[+_].
Let's continue. We can recognize the single character 'a', but what if we want
to recognize the string "abracadabra"? We don't have a way of recognizing
entire strings right now, so let's add that:
www.it-ebooks.info
148
But choosing between two parsers seems like something that would be more
generally useful, regardless of their result type, so let's go ahead and make this
polymorphic:
We have also made string an implicit conversion and added another implicit
asStringParser. With these two functions, Scala will automatically promote a
String to a Parser, and we get infix operators for any type, A that can be
converted to a Parser[String]. So given a val P: Parsers, we can then
import P._ to let us write expressions like "abra" or "cadabra" or "a"
| "bbb" to create parsers. This will work for all implementations of Parsers.7
Other binary operators or methods can be added to the body of ParserOps. We
are going to follow the discipline of keeping the primary definition directly in
Parsers and delegating in ParserOps to this primary definition. See the code
for this chapter for more examples. We'll be using the a | b syntax liberally
throughout the rest of this chapter to mean or(a,b).
Footnote 7mSee the appendix Scalaz, implicits, and large library organization for more discussion of these
issues.
www.it-ebooks.info
149
We can now recognize various strings, but we don't have any way of talking
about repetition. For instance, how would we recognize three repetitions of our
"abra" | "cadabra" parser? Once again, let's add a combinator for it:8
Footnote 8mThis should remind you of a very similar function we wrote in the previous chapter.
A Parser[Int] that recognizes zero or more 'a' characters, and whose result value is
the number of 'a' characters it has seen. For instance, given "aa", the parser results in 2,
given "" or "b123" (a string not starting with 'a'), it results in 0, and so on.
A Parser[Int] that recognizes one or more 'a' characters, and whose result value is the
number of 'a' characters it has seen. (Is this defined somehow in terms of the same
combinators as the parser for 'a' repeated zero or more times?) The parser should fail
when given a string without a starting 'a'. How would you like to handle error reporting
in this case? Could the API support giving an explicit message like "Expected one or
more 'a'" in the case of failure?
A parser that recognizes zero or more 'a', followed by one or more 'b', and which
results in the pair of counts of characters seen. For instance, given "bbb", we get (0,3),
given "aaaab", we get (4,1), and so on.
If we are trying to parse a sequence of zero or more "a" and are only interested in the
number of characters seen, it seems inefficient to have to build up, say, a List[Char]
only to throw it away and extract the length. Could something be done about this?
Are the various forms of repetition primitive in your algebra, or could they be defined in
terms of something simpler?
We introduced a type ParseError earlier, but so far we haven't chosen any functions for
www.it-ebooks.info
150
the API of ParseError and our algebra doesn't have any ways of letting the programmer
control what errors are reported. This seems like a limitation given that we'd like
meaningful error messages from our parsers. Can you do something about it?
Does a | b mean the same thing as b | a? This is a choice you get to make. What are
the consequences if the answer is yes? What about if the answer is no?
Does a | (b | c) mean the same thing as (a | b) | c? If yes, is this a primitive law
for your algebra, or is it implied by something simpler?
Try to come up with a set of laws to specify your algebra. You don't necessarily need the
laws to be complete, just write down some laws that you expect should hold for any
Parsers implementation.
Spend some time coming up with combinators and possible laws based on this
guidance. When you feel stuck or at a good stopping point, then continue reading
to the next section, which walks through a possible design.
www.it-ebooks.info
151
First, let's consider the parser that recognizes zero or more repetitions of the
character 'a', and returns the number of characters it has seen. We can start by
adding a primitive combinator for it, let's call it many:
This isn't quite right, though—we need a Parser[Int] that counts the
number of elements. We could change the many combinator to return a
Parser[Int], but that feels a little too specific—undoubtedly there will be
occasions where we do care about more than just the list length. Better to introduce
another combinator that should be familiar by now, map:
map(p)(id) == p
How should we document this law that we've just added? We could put it in a
documentation comment, but in the preceding chapter we developed a way to make
our laws executable. Let's make use of that library here:
www.it-ebooks.info
152
import fpinscala.testing._
trait Parsers {
...
object Laws {
def equal[A](p1: Parser[A], p2: Parser[A])(in: Gen[String]): Prop =
forAll(in)(s => run(p1)(s) == run(p2)(s))
This will come in handy later when we go to test that our implementation of
Parsers behaves as we expect. As we discuss other laws, you are encouraged to
write them out as actual properties inside the Laws object.10
Footnote 10mAgain, see the chapter code for more examples. In the interest of keeping this chapter shorter,
we won't be giving Prop implementations of all the laws, but that doesn't mean you shouldn't try writing
them out yourself!
Incidentally, now that we have map, we can actually implement char in terms
of string:
This parser always succeeds with the value a, regardless of the input string
(since string("") will always succeed, even if the input is empty). Does this
combinator seem familiar to you? We can specify its behavior with a law:
run(succeed(a))(s) == Right(a)
www.it-ebooks.info
153
We call it slice since it returns the portion of the input string examined by
the parser if successful. As an example,
run(slice(or('a','b').many))("aaba") results in
Right("aaba")—that is, we ignore the list accumulated by many and simply
return the portion of the input string matched by the parser.
With slice , our parser can now be written as
char('a').many.slice.map(_.length) (again, assuming we add an
alias for slice to ParserOps). The _.length function here is now
referencing the length method on String, rather than the length method on
List.
Let's consider the next use case. What if we want to recognize one or more 'a'
characters? First, we introduce a new combinator for it, many1:
It feels like many1 should not have to be primitive, but should be defined
somehow in terms of many. Really, many1(p) is just p followed by many(p).
So it seems we need some way of running one parser, followed by another,
assuming the first is successful. Let's add that:
www.it-ebooks.info
154
and then use this to implement many1 in terms of many. (Note that we could have
chosen to make map2 primitive and defined product in terms of map2 as we've
done in the previous chapters)
With many1, we can now implement the parser for zero or more 'a' followed
by one or more 'b' as:
char('a').many.slice.map(_.length) **
char('b').many1.slice.map(_.length)
This looks pretty, but there's a problem with it. Can you spot what it is? We are
calling many recursively in the second argument to map2, which is strict in
www.it-ebooks.info
155
many(p)
map2(p, many(p))(_ :: _)
map2(p, map2(p, many(p))(_ :: _))(_ :: _)
map2(p, map2(p, map2(p, many(p))(_ :: _))(_ :: _))(_ :: _)
...
Because a call to map2 always evaluates its second argument, our many
function will never terminate! That's no good. Let's go ahead and make product
and map2 non-strict in their second argument:
www.it-ebooks.info
156
EXERCISE 6: Given this choice of meaning for or, is it associative? That is,
should a or (b or c) equal (a or b) or c for all choices of a, b, and c?
We'll come back to this question when refining the laws for our algebra.
www.it-ebooks.info
157
Can you see how this signature implies an ability to sequence parsers?
EXERCISE 8: Using flatMap and any other combinators, write the
context-sensitive parser we could not express above. To parse the digits, you can
make use of a new primitive, regex, which promotes a regular expression to a
Parser:12 In Scala, given a string, s, it can be promoted to a Regex object
(which has methods for performing matching) using s.r, for instance:
"[a-zA-Z_][a-zA-Z0-9_]*".r
Footnote 12mIn theory this isn't necessary, we could write out "0" | "1" | ... "9" to recognize a
single digit, but this isn't likely to be very efficient.
www.it-ebooks.info
158
EXERCISE 11 (hard): If you have not already done so, spend some time
discovering a nice set of combinators for expressing what errors get reported by a
Parser. For each combinator, try to come up with laws specifying what its
behavior should be. This is a very open-ended design task. Here are some guiding
questions:
Once you are satisfied with your design, you can continue reading. The next
section works through a possible design in detail.
www.it-ebooks.info
159
A POSSIBLE DESIGN
Now that you've spent some time coming up with some good error reporting
combinators, we are now going to work through one possible design. Again, you
may have arrived at a different design, which is absolutely fine. This is just another
opportunity to see a worked design process.
We are going to progressively introduce our error reporting combinators. To
start, let's introduce an obvious one. None of the primitives so far let us assign an
error message to a parser. We can introduce a primitive combinator for this,
label:
We've picked a concrete representation for Location here that includes the
full input, an offset into this input, and the line and column numbers (which can be
computed, lazily, derived from the full input and offset). We can now say more
precisely what we expect from label—in the event of failure with Left(e),
errorMessage(e) will equal the message set by label. This can be specified
with a Prop if we like:
www.it-ebooks.info
160
What about the Location? We would like for this to be filled in by the
Parsers implementation with the location where the error occurred. This notion
is still a bit fuzzy—if we have a or b and both parsers fail on the input, which
location is reported, and which label(s)? We will discuss this in the next section.
ERROR NESTING
Is the label combinator sufficient for all our error reporting needs? Not quite.
Let's look at an example:
Skip whitespace
www.it-ebooks.info
161
Unlike label, scope does not throw away the label(s) attached to p—it
merely adds additional information in the event that p fails. Let's specify what this
means exactly. First, we modify the functions that pull information out of a
ParseError. Rather than containing just a single Location and String
message, we should get a List[(Location,String)]:
This is a stack of error messages indicating what the Parser was doing when
it failed. We can now specify what scope does—if run(p)(s) is Left(e),
then run(scope(msg)(p)) is Left(e2), where errorStack(e2) will
have at the top of the stack the message msg, followed by any messages added by
p itself.
We can take this one step further. A stack does not fully capture what the parser
was doing at the time it failed. Consider the parser scope("abc")(a or b
or c). If a, b, and c all fail, which error goes at the top of the stack? We could
adopt some global convention, like always reporting the last parser's error (in this
case c) or perhaps reporting whichever parser examined more of the input, but it
might be nice to allow the implementation to return all the errors if it chooses:
This is a somewhat unusual data structure—we have stack, the current stack,
but also a list of other failures (otherFailures) that occurred previously in a
chain of or combinators.13 This is potentially a lot of information, capturing not
only the current path in the grammar, but also all the previous failing paths. We
can write helper functions later to make constructing and manipulating
ParseError values more convenient and to deal with formatting them nicely for
human consumption. For now, our concern is just making sure it contains all the
potentially relevant information for error reporting, and it seems like
ParseError will be more than sufficient. Let's go ahead and pick this as our
concrete representation. We can remove the type parameter from Parsers:
www.it-ebooks.info
162
Footnote 13mWe could also represent ParseError as a trie in which shared prefixes of the error stack are
not duplicated, at a cost of having more expensive inserts. It is easier to recover this sharing information during
formatting of errors, which happens only once.
trait Parsers[Parser[+_]] {
def run[A](p: Parser[A])(input: String): Either[ParseError,A]
...
}
Now we are giving the Parsers implementation all the information it needs
to construct nice, hierarchical errors if it chooses. As a user of Parsers, we will
judiciously sprinkle our grammar with label and scope calls which the
Parsers implementation can use when constructing parse errors. Note that it
would be perfectly reasonable for implementations of Parsers to not use the full
power of ParseError and retain only basic information about the cause and
location of errors.14
Footnote 14mWe may want to explicitly acknowledge this by relaxing the laws specified for Parsers
implementations, or making certain laws optional.
www.it-ebooks.info
163
Where fail is a parser that always fails (we could introduce this as a primitive
combinator if we like). That is, even if p fails midway through examining the
input, attempt reverts the commit to that parse and allows p2 to be run. The
attempt combinator can be used whenever there is ambiguity in the grammar
and multiple tokens may have to be examined before the ambiguity can be resolved
and parsing can commit to a single branch. As an example, we might write:
www.it-ebooks.info
164
We've used these primitives to define a number of combinators like map, map2
, flatMap, many, and many1.
Again, we will be asking you to drive the process of writing this parser. After a
www.it-ebooks.info
165
brief introduction to JSON and the data type we'll use for the parse result, it's up to
you to go off on your own to write the parser.
{
"Company name" : "Microsoft Corporation",
"Ticker" : "MSFT",
"Active" : true,
"Price" : 30.66,
"Shares outstanding" : 8.38e9,
"Related companies" :
[ "HPQ", "IBM", "YHOO", "DELL", "GOOG" ]
}
trait JSON
object JSON {
case object JNull extends JSON
case class JNumber(get: Double) extends JSON
case class JString(get: String) extends JSON
case class JBool(get: Boolean) extends JSON
case class JArray(get: IndexedSeq[JSON]) extends JSON
case class JObject(get: Map[String, JSON]) extends JSON
}
The task is two write a Parser[JSON]. We want this to work for whatever
the chosen Parsers implementation. To allow this, we can place the
implementation in a regular function that takes a Parsers value as its argument:
www.it-ebooks.info
166
18
Footnote 18mUnfortunately, we have to mention the covariance of the Parser type constructor again, rather
than inferring the variance from the definition of Parsers.
EXERCISE 13 (hard): At this point, you are going to take over the process.
You will be creating a Parser[JSON] from scratch using the primitives we have
defined. You don't need to worry (yet) about the representation of Parser. As
you go, you will undoubtedly discover additional combinators and idioms, notice
and factor out common patterns, and so on. Use the skills and knowledge you've
been developing throughout this book, and have fun! If you get stuck, you can
always consult the answers.
Here is some minimal guidance:
Any general purpose combinators you discover can be added to the Parsers trait
directly.
You will probably want to introduce combinators that make it easier to parse the tokens
of the JSON format (like string literals and numbers). For this you could use the regex
primitive we introduced earlier. You could also add a few primitives like letter, digit,
whitespace, and so on, for building up your token parsers.
www.it-ebooks.info
167
Replace MyParser with whatever data type you use for representing your
parsers. When you have something you are satisfied with, get stuck, or want some
more ideas, keep reading.
On the left side we have a Parser[A], but on the right side we have a
function. The expression flatMap(a,flatMap(b,c)) would not even be
www.it-ebooks.info
168
This might seem like a rather odd-looking combinator, but it can be used to
express flatMap, if we substitute Unit in for U:20
Footnote 20mRecall that the type Unit has just a single value, written (). That is, val u: Unit = ().
The key insight here was that Unit => Parser[A] could be converted to
Parser[A] and vice versa.
EXERCISE 15: To complete the demonstration that seq and flatMap are
equivalent in expressiveness, define seq in terms of flatMap.
Now that we have an operation with the same shape on both sides, we can ask
whether it should be associative as well. That is, do we expect the following:
EXERCISE 16 (hard): That answer is again, yes, but can you see why? Think
about this law and write down an explanation in your own words of why it should
hold, and what it means for a Parser. We will discuss this more in chapter 11.
EXERCISE 17 (hard): Come up with a law to specify the relationship between
seq and succeed.
EXERCISE 18: The seq and succeed laws specified are quite common and
arise in many different guises. Choose a data type, such as Option or List.
Define functions analogous to seq and succeed for this type and show that the
implementations satisfy the same laws. We will discuss this connection further in
chapter 11.
EXERCISE 19 (hard, optional): We can define a function, kor, analogous to
seq but using or to combine the results of the two functions:
www.it-ebooks.info
169
Can you think of any laws to specify the relationship between seq and kor?
As a first guess, we can assume that our Parser is simply the implementation
of the run function:
www.it-ebooks.info
170
trait Result[+A]
case class Success[+A](get: A, charsConsumed: Int) extends Result[A]
case class Failure(get: ParseError) extends Result[Nothing]
We introduced a new type here, Result, rather than just using Either. In
the event of success, we return a value of type A as well as the number of
characters of input consumed, which can be used by the caller to update the
Location state.22. This type is starting to get at the essence of what a Parser
is—it is a kind of state action that can fail, similar to what we built in chapter 6.23
It receives an input state, and if successful returns a value as well enough
information to control how the state should be updated.
Footnote 22mNote that returning an (A,Location) would give Parser the ability to set the input,
which is granting it too much power.
Footnote 23mThe precise relationship between these two types will be further explored in part 3 when we
discuss what are called monad transformers.
www.it-ebooks.info
171
flatMap(p)(f): Run a parser, then use its result to select a second parser to run in
sequence
attempt(p): Delays committing to p until after it succeeds
or(p1,p2): Chooses between two parsers, first attempting p1, then p2 if p1 fails in an
uncommitted state on the input
Because we push onto the stack after the inner parser has returned, the bottom
of the stack will have more detailed messages that occurred later in parsing.
(Consider the ParseError that will result if scope(msg1)(a **
scope(msg2)(b)) fails while parsing b.)
And we can implement label similarly. In the event of failure, we want to
www.it-ebooks.info
172
replace the failure message. We can write this again using mapError:
www.it-ebooks.info
173
We can support the behavior we want by adding one more piece of information
to the Failure case of Result—a Boolean value indicating whether the
parser failed in a committed state:
www.it-ebooks.info
174
EXERCISE 22: Implement the rest of the primitives, including run using this
representation of Parser and try running your JSON parser on various inputs.25
Footnote 25mYou will find, unfortunately, that it stack overflows for large inputs (for instance,
[1,2,3,...10000]). One simple solution to this is to provide a specialized implementation of many that
avoids using a stack frame for each element of the list being built up. So long as any combinators that do repetition
are defined in terms of many (which they all can be), this solves the problem. See the answers for discussion
of more general approaches.
www.it-ebooks.info
175
9.6 Conclusion
In this chapter, we introduced an approach to writing combinator libraries called
algebraic design, and used it to design a parser combinator library and implement a
JSON parser. Along the way, we discovered a number of very similar combinators
to previous chapters, which again were related by familiar laws. In part 3, we will
finally understand the nature of the connection between these libraries and learn
how to abstract over their common structure.
This is the final chapter in part 2. We hope you have come away from these
chapters with a basic sense for how functional design can proceed, and more
importantly, we hope these chapters have motivated you to try your hand at
designing your own functional libraries, for whatever domains interest you.
Functional design is not something reserved only for FP experts—it should be part
of the day-to-day work done by functional programmers at all experience levels.
Before starting on part 3, we encourage you to venture beyond this book and try
writing some more functional code and designing some of your own libraries.
Have fun, enjoy struggling with design problems that come up, and see what you
discover. Next we will begin to explore the universe of patterns and abstractions
which the chapters so far have only hinted at.
Index Terms
algebra
algebraic design
backtracking
combinator parsing
commit
laws
parser combinators
www.it-ebooks.info
176
10
By the end of Part 2, we were getting comfortable with considering data types in
terms of their algebras—that is, the operations they support and the laws that
govern those operations. Hopefully you will have noticed that the algebras of very
Monoids
different data types tend to follow certain patterns that they have in common. In
this chapter, we are going to begin identifying these patterns and taking advantage
of them. We will consider algebras in the abstract, by writing code that doesn't just
operate on one data type or another but on all data types that share a common
algebra.
The first such abstraction that we will introduce is the monoid1. We choose to
start with monoids because they are very simple and because they are ubiquitous.
Monoids come up all the time in everyday programming, whether we're aware of
them or not. Whenever you are working with a list or concatenating strings or
accumulating the result of a loop, you are almost certainly using a monoid.
Footnote 1mThe name "monoid" comes from mathematics. The prefix "mon-" means "one", and in category
theory a monoid is a category with one object. See the chapter notes for more information.
www.it-ebooks.info
177
Some type A
A binary associative operation that takes two values of type A and combines them into
one.
A value of type A that is an identity for that operation.
trait Monoid[A] {
def op(a1: A, a2: A): A
def zero: A
}
www.it-ebooks.info
178
EXERCISE 3: A function having the same argument and return type is called
an endofunction2. Write a monoid for endofunctions:
Footnote 2mThe Greek prefix "endo-" means "within", in the sense that an endofunction's codomain is within
its domain.
www.it-ebooks.info
179
The components of a monoid fit these argument types like a glove. So if we had
a list of Strings, we could simply pass the op and zero of the
stringMonoid in order to reduce the list with the monoid.
www.it-ebooks.info
180
But what if our list has an element type that doesn't have a Monoid instance?
Well, we can always map over the list to turn it into a type that does.
www.it-ebooks.info
181
This parallelism might give us some efficiency gains, because the two inner op
s could be run simultaneously in separate threads.
If we split this string roughly in half, we might split it in the middle of a word.
In the case of our string above, that would yield "lorem ipsum do" and "lor
sit amet, ". When we add up the results of counting the words in these
strings, we want to avoid double-counting the word dolor. Clearly, just counting
the words as an Int is not sufficient. We need to find a data structure that can
handle partial results like the half words do and lor, and can track the complete
words seen so far, like ipsum, sit, and amet.
The partial result of the word count could be represented by an algebraic data
www.it-ebooks.info
182
type:
sealed trait WC
case class Stub(chars: String) extends WC
case class Part(lStub: String, words: Int, rStub: String) extends WC
A Stub is the simplest case, where we have not seen any complete words yet.
But a Part keeps the number of complete words we have seen so far, in words.
The value lStub holds any partial word we have seen to the left of those words,
and rStub holds the ones on the right.
For example, counting over the string "lorem ipsum do" would result in
Part("lorem", 1, "do") since there is one complete word. And since there
is no whitespace to the left of lorem or right of do, we can't be sure if they are
complete words, so we can't count them yet. Counting over "lor sit amet,
" would result in Part("lor", 2, "").
EXERCISE 9: Write a monoid instance for WC and make sure that it meets the
monoid laws.
EXERCISE 10: Use the WC monoid to implement a function that counts words
in a String by recursively splitting it into substrings and counting the words in
those substrings.
www.it-ebooks.info
183
Footnote 4mThis word comes from Greek, "homo" meaning "same" and
"morphe" meaning "shape".
The same law holds for the homomorphism from String to WC in the
current example.
This property can be very useful when designing your own libraries. If
two types that your library uses are monoids, and there exist functions
between them, it's a good idea to think about whether those functions
are expected to preserve the monoid structure and to check the monoid
homomorphism law with automated tests.
There is a higher-order function that can take any function of type A =>
B, where B is a monoid, and turn it in to a monoid homomorphism from
List[A] to B.
Sometimes there will be a homomorphism in both directions between
two monoids. Such a relationship is called a monoid isomorphism ("iso-"
meaning "equal") and we say that the two monoids are isomorphic.
www.it-ebooks.info
184
At every step of the fold, we are allocating the full intermediate String only
to discard it and allocate a larger string in the next step:
A more efficient strategy would be to combine the list by halves. That is, to first
construct "loremipsum" and "dolorsit", then add those together. But a
List is inherently sequential, so there is not an efficient way of splitting it in half.
Fortunately, there are other structures that allow much more efficient random
access, such as the standard Scala library's Vector data type which provides very
efficient length and splitAt methods.
EXERCISE 11: Implement an efficient foldMap for IndexedSeq, a
common supertype for various data structures that provide efficient random access.
ints.foldRight(0)(_ + _)
www.it-ebooks.info
185
Looking at just this code snippet, we don't really know the type of ints. It
could be a Vector, a Stream, or a List, or anything at all with a foldRight
method. We can capture this commonality in a trait:
trait Foldable[F[_]] {
def foldRight[A, B](as: F[A])(f: (A, B) => B): B
def foldLeft[A, B](as: F[A])(f: (B, A) => B): B
def foldMap[A, B](as: F[A])(f: A => B)(mb: Monoid[B]): B
def concatenate[A](as: F[A])(m: Monoid[A]): A =
as.foldLeft(m.zero)(m.op)
}
Here we are abstracting over a type constructor F, much like we did with the
Parser type in the previous chapter. We write it as F[_], where the underscore
indicates that F is not a type but a type constructor that takes one type argument.
Just like functions that take other functions as arguments are called higher-order
functions, something like Foldable is a higher-order type constructor or a
higher-kinded type5.
Footnote 5mJust like values and functions have types, types and type constructors have kinds. Scala uses kinds
to track how many type arguments a type constructor takes, whether it is co- or contravariant in those arguments,
and what their kinds are.
www.it-ebooks.info
186
EXERCISE 18: Do the same with Either. This is called a monoid coproduct.
www.it-ebooks.info
187
additional programming:
EXERCISE 19: Write a monoid instance for functions whose results are
monoids.
A frequency map contains one entry per word, with that word as the key, and
the number of times that word appears as the value under that key. For example:
www.it-ebooks.info
188
All we have to do is specify the type of the monoid we want. As long as that
type has an implicit Monoid instance, it will be returned to us.
But what about types that have more than once Monoid instance? For
example, a valid monoid for Int could be either addition (with 0 as the identity)
or multiplication (with 1 as the identity). A common solution is to put values in a
simple wrapper type and make that type the monoid.
www.it-ebooks.info
189
This plays nicely with foldMap on Foldable. For example if we have a list
ints of integers and we want to sum them:
listFoldable.foldMap(ints)(Sum(_))
listFoldable.foldMap(ints)(Product(_))
10.6 Summary
In this chapter we introduced the concept of a monoid, a simple and common type
of abstract algebra. When you start looking for it, you will find ample opportunity
to exploit the monoidal structure of your own libraries. The associative property
lets you fold any Foldable data type and gives you the flexibility of doing so in
parallel. Monoids are also compositional, and therefore they let you write folds in a
declarative and reusable way.
Monoid has been our first totally abstract trait, defined only in terms of its
abstract operations and the laws that govern them. This gave us the ability to write
useful functions that know nothing about their arguments except that their type
happens to be a monoid.
Index Terms
associativity
higher-kinded types
identity element
implicit instances
monoid
monoid coproduct
monoid homomorphism
monoid laws
monoid product
type constructor polymorphism
www.it-ebooks.info
190
11 Monads
These type signatures are very similar. The only difference is the concrete type
involved. We can capture, as a Scala trait, the idea of "a data type that implements
map".
trait Functor[F[_]] {
def map[A,B](fa: F[A])(f: A => B): F[B]
}
www.it-ebooks.info
191
Much like we did with Foldable in the previous chapter, we introduce a trait
Functor that is parameterized on a type constructor F[_]. Here is an instance
for List:
What can we do with this abstraction? There happens to be a small but useful
library of functions that we can write using just map. For example, if we have
F[(A, B)] where F is a functor, we can "distribute" the F over the pair to get
(F[A], F[B]):
It's all well and good to introduce a new combinator like this in the abstract, but
we should think about what it means for concrete data types like Gen, Option,
etc. For example, if we distribute a List[(A, B)], we get two lists of the
same length, one with all the As and the other with all the Bs. That operation is
sometimes called "unzip". So we just wrote a generic unzip that works not just for
lists, but for any functor!
Whenever we create an abstraction like this, we should consider not only what
abstract methods it should have, but which laws we expect should hold for the
implementations. If you remember, back in chapter 7 (on parallelism) we found a
general law for map:
map(v)(x => x) == v
Later in Part 2, we found that this law is not specific to Par. In fact, we would
expect it to hold for all implementations of map that are "structure-preserving".
This law (and its corollaries given by parametricity) is part of the specification of
what it means to "map". It would be very strange if it didn't hold. It's part of what a
functor is.
www.it-ebooks.info
192
def map2[A,B,C](
ga: Gen[A], gb: Gen[B])(f: (A,B) => C): Gen[C] =
ga flatMap (a => fb map (b => f(a,b)))
def map2[A,B,C](
pa: Parser[A], pb: Parser[B])(f: (A,B) => C): Parser[C] =
pa flatMap (a => pb map (b => f(a,b)))
def map2[A,B,C](
pa: Option[A], pb: Option[B])(f: (A,B) => C): Option[C] =
pa flatMap (a => pb map (b => f(a,b)))
These functions have more in common than just the name. In spite of operating
on data types that seemingly have nothing to do with one another, the
implementations are exactly identical! And again the only thing that differs in the
type signatures is the particular data type being operated on. This confirms what
we have been suspecting all along—that these are particular instances of some
more general pattern. We should be able to exploit that fact to avoid repeating
ourselves. For example, we should be able to write map2 once and for all in such a
way that it can be reused for all of these data types.
We've made the code duplication particularly obvious here by choosing
uniform names for our functions, taking the arguments in the same order, and so
on. It may be more difficult to spot in your everyday work. But the more
combinator libraries you write, the better you will get at identifying patterns that
you can factor out into a common abstraction. In this chapter, we will look at an
abstraction that unites Parser, Gen, Par, Option, and some other data types
we have already looked at: They are all monads. We will explain in a moment
exactly what that means.
www.it-ebooks.info
193
So let's pick unit and flatMap as our minimal set. We will unify under a
single concept all data types that have these combinators defined. Monad has
flatMap and unit abstract, and provides default implementations for map and
map2.
Since Monad provides a default implementation of map, it can extend Functor. All
monads are functors, but not all functors are monads.
To tie this back to a concrete data type, we can implement the Monad instance
for Gen:
www.it-ebooks.info
194
object Monad {
val genMonad = new Monad[Gen] {
def unit[A](a: => A): Gen[A] = Gen.unit(a)
def flatMap[A,B](ma: Gen[A])(f: A => Gen[B]): Gen[B] =
ma flatMap f
}
}
We only need to implement unit and flatMap and we get map and map2 at
no additional cost. We have implemented them once and for all, for any data type
for which it is possible to supply an instance of Monad! But we're just getting
started. There are many more combinators that we can implement once and for all
in this manner.
EXERCISE 1: Write monad instances for Par, Parser, Option, Stream,
and List.
EXERCISE 2 (optional, hard): State looks like it would be a monad too, but
it takes two type arguments and you need a type constructor of one argument to
implement Monad. Try to implement a State monad, see what issues you run
into, and think about possible solutions. We will discuss the solution later in this
chapter.
One combinator we saw for e.g. Gen and Parser was listOfN, which
allowed us to replicate a parser or generator n times to get a parser or generator of
www.it-ebooks.info
195
lists of that length. We can implement this combinator for all monads M by adding
it to our Monad trait. We should also give it a more generic name such as
replicateM.
EXERCISE 4: Implement replicateM:
EXERCISE 5: Think about how replicateM will behave for various choices
of M. For example, how does it behave in the List monad? What about Option?
Describe in your own words the general meaning of replicateM.
There was also a combinator product for our Gen data type to take two
generators and turn them into a generator of pairs, and we did the same thing for
Par computations. In both cases, we implemented product in terms of map2.
So we can definitely write it generically for any monad M. This combinator might
more appropriately be called factor, since we are "factoring out" M, or "pushing
M to the outer layer":
def factor[A,B](ma: M[A], mb: M[B]): M[(A, B)] = map2(ma, mb)((_, _))
www.it-ebooks.info
196
Above, we are generating the Item inline, but there might be places where we
want to generate an Item separately. So we could pull that into its own generator:
www.it-ebooks.info
197
And that should do exactly the same thing, right? It seems safe to assume that.
But not so fast. How can we be sure? It's not exactly the same code.
EXERCISE 7 (optional): Expand both implementations of genOrder to map
and flatMap calls to see the true difference.
Once you expand them out, those two implementations are clearly not identical.
And yet when we look at the for-comprehension, it seems perfectly reasonable to
assume that the two implementations do exactly the same thing. In fact, it would be
surprising and weird if they didn't. It's because we are assuming that flatMap
obeys an associative law:
And this law should hold for all values x, f, and g of the appropriate types.
EXERCISE 8: Prove that this law holds for Option.
EXERCISE 9 (hard): Show that the equivalence of the two genOrder
implementations above follows from this law.
But our associative law for monads doesn't look anything like that! Fortunately,
there's a way we can make the law clearer if we consider not the monadic values of
types like M[A], but monadic functions of types like A => M[B]. Functions like
that are called Kleisli arrows1, and they can be composed with one another:
Footnote 1mThis name comes from category theory and is after the Swiss mathematician Heinrich Kleisli.
www.it-ebooks.info
198
This function has exactly the right type to be passed to compose. The effect
should be that anything composed with unit is that same thing. This usually takes
the form of two laws, left identity and right identity:
compose(f, unit) == f
compose(unit, f) == f
www.it-ebooks.info
199
And we know that there are two monad laws to be satisfied, associativity and
identity, that can be formulated in various ways. So we can state quite plainly what
a monad is:
A monad is an implementation of one of the minimal sets of monadic
combinators, satisfying the laws of associativity and identity.
That's a perfectly respectable, precise, and terse definition. But it's a little
dissatisfying. It doesn't say very much about what it implies—what a monad
means. The problem is that it's a self-contained definition. Even if you're a
beginning programmer, you have by now obtained a vast amount of knowledge
about programming, and this definition integrates with none of that. In order to
really understand what's going on with monads (or with anything for that matter),
we need to think about them in terms of things we already understand. We need to
connect this new knowledge into a wider context.
To say "this data type is a monad" is to say something very specific about how
www.it-ebooks.info
200
it behaves. But what exactly? To begin to answer the question of what monads
mean, let's look at another couple of monads and compare their behavior.
EXERCISE 19: Implement map and flatMap as methods on this class, and
give an implementation for Monad[Id].
Now, Id is just a simple wrapper. It doesn't really add anything. Applying Id
to A is an identity since the wrapped type and the unwrapped type are totally
isomorphic (we can go from one to the other and back again without any loss of
information). But what is the meaning of the identity monad? Let's try using it in
the REPL:
scala> for {
| a <- Id("Hello, ")
| b <- Id("monad!")
| } yield a + b
res1: Id[java.lang.String] = Id(Hello, monad!)
So what is the action of flatMap for the identity monad? It's simply variable
substitution. The variables a and b get bound to "Hello, " and "monad!",
respectively, and then they get substituted into the expression a + b. In fact, we
could have written the same thing without the Id wrapper, just using Scala's own
variables:
www.it-ebooks.info
201
scala> a + b
res2: java.lang.String = Hello, monad!
It looks like State definitely fits the profile for being a monad. But its type
constructor takes two type arguments and Monad requires a type constructor of
one argument, so we can't just say Monad[State]. But if we choose some
particular S then we have something like State[S, _], which is the kind of
thing expected by Monad. So State doesn't just have one monad instance but a
whole family of them, one for each choice of S. We would like to be able to
partially apply State to where the S type argument is fixed to be some concrete
type.
This is very much like how you would partially apply a function, except at the
type level. For example, we can create an IntState type constructor, which is an
alias for State with its first type argument fixed to be Int:
www.it-ebooks.info
202
And IntState is exactly the kind of thing that we can build a Monad for:
This syntax can be a little jarring when you first see it. But all we are doing is
declaring an anonymous type within parentheses. This anonymous type has, as one
of its members, the type alias IntState, which looks just like before. Outside
the parentheses we are then accessing its IntState member with the # syntax.
Just like we can use a "dot" (.) to access a member of an object at the value level,
we can use the # symbol to access a type member (See the "Type Member" section
of the Scala Language Specification).
A type constructor declared inline like this is often called a type lambda in
Scala. We can use this trick to partially apply the State type constructor and
declare a StateMonad trait. An instance of StateMonad[S] is then a monad
instance for the given state type S.
www.it-ebooks.info
203
This function numbers all the elements in a list using a State action. It keeps
a state that is an Int which is incremented at each step. The whole composite state
action is run starting from 0. We are then reversing the result since we constructed
it in reverse order2.
Footnote 2mThis is asymptotically faster than appending to the list in the loop.
The details of this code are not really important. What is important is what's
www.it-ebooks.info
204
object Reader {
def readerMonad[R] = new Monad[({type f[x] = Reader[R,x]})#f] {
def unit[A](a: => A): Reader[R,A]
def flatMap[A,B](st: Reader[R,A])(f: A => Reader[R,B]): Reader[R,B]
}
}
www.it-ebooks.info
205
11.6 Conclusion
In this chapter, we took a pattern that we had seen repeated throughout the book
and we unified it under a single concept: monad. This allowed us to write a number
of combinators once and for all, for many different data types that at first glance
don't seem to have anything in common. We discussed laws that they all satisfy,
the monad laws, from various perspectives, and we tried to gain some insight into
what it all means.
An abstract topic like this cannot be fully understood all at once. It requires an
iterative approach where you keep revisiting the topic from new perspectives.
When you discover new monads, new applications of them, or see them appear in a
new context, you will inevitably gain new insight. And each time it happens you
might think to yourself: "OK, I thought I understood monads before, but now I
really get it."
In the next chapter, we will explore a slight variation on the theme of monads,
and develop your ability to discover new abstractions on your own.
Index Terms
functor
functor law
identity element
Kleisli arrow
left identity law for monads
listOfN
monad
monadic join
monad identity law
monad laws
replicateM
right identity law for monads
type lambda
unit law
www.it-ebooks.info
206
www.it-ebooks.info
207
What you may not have noticed is that almost all of the useful combinators we
wrote in the previous chapter can be implemented just in terms of map2 and unit
. What's more, for all the data types we have discussed, map2 can be written
without resorting to flatMap.
Is map2 with unit then just another minimal set of operations for monads?
No it's not, because there are monadic combinators such as join and flatMap
that cannot be implemented with just map2 and unit. This suggests that map2
and unit is a less powerful subset of the Monad interface, that nonetheless is
very useful on its own.
You should already have a good sense of what map2 means. But what is the
www.it-ebooks.info
208
meaning of apply? At this point it's a little bit of a floating abstraction, and we
need an example. Let's drop down to Option and look at a concrete
implementation:
You can see that this method combines two Options. But one of them
contains a function (unless it is None of course). The action of apply is to apply
the function inside one argument to the value inside the other. This is the origin of
the name "applicative". This operation is sometimes called idiomatic function
application since it occurs within some idiom or context. In the case of the
Identity idiom from the previous chapter, the implementation is literally just
function application. In the example above, the idiom is Option so the method
additionally encodes the rule for None, which handles the case where either the
function or the value are absent.
The action of apply is similar to the familiar map. Both are a kind of function
application in a context, but there's a very specific difference. In the case of
apply, the function being applied might be affected by the context. For example,
if the second argument to apply is None in the case of Option then there is no
function at all. But in the case of map, the function must exist independently of the
context. This is easy to see if we rearrange the type signature of map a little and
compare it to apply:
The only difference is the F around the function argument type. The apply
function is strictly more powerful, since it can have that added F effect. It makes
sense that we can implement map in terms of apply, but not the other way
around.
We have also seen that we can implement map2 as well in terms of apply.
We can extrapolate that pattern and implement map3, map4, etc. In fact, apply
can be seen as a general function lifting combinator. We can use it to lift a function
www.it-ebooks.info
209
of any arity into our applicative functor. Let's look at map3 on Option as an
example.
We start out with a ternary function like (_ + _ + _), which just adds 3
integers:
If we Curry this function, we get a slightly different type. It's the same function,
but one that can be gradually applied to arguments one at a time.
Pass that to unit (which is just Some in the case of Option) and we have:
We can now use idiomatic function application three times to apply this to three
Option values. To complete the picture, here is the fully general implementation
of map3 in terms of apply.
The pattern is simple. We just Curry the the function we want to lift, pass the
result to unit, and then apply as many times as there are arguments. Each call
to apply is a partial application of the function1.
Footnote 1mNotice that in this sense unit can be seen as map0, since it's the case of "lifting" where apply
is called zero times.
www.it-ebooks.info
210
But the converse is not true—not all applicative functors are monads. Using
unit together with either map2 or apply, we cannot implement flatMap or
join. If we try it, we get stuck pretty quickly.
Let's look at the signatures of flatMap and apply side by side, rearranging
the signature of flatMap slightly from what we're used to in order to make things
clearer:
The difference is in the type of the function argument. In apply, that argument
is fully contained inside F. So the only way to pass the A from the second
argument to the function of type A => B in the first argument is to somehow
consider both F contexts. For example, if F is Option we pattern-match on both
arguments to determine if they are Some or None:
www.it-ebooks.info
211
Likewise with map2 the function argument f is only invoked if both of the
Option arguments are Some. The important thing to note is that whether the
answer is Some or None is entirely determined by whether the inputs are both
Some.
But in flatMap, the second argument is a function that produces an F, and its
structure depends on the value of the A from the first argument. So in the Option
case, we would match on the first argument, and if it's Some we apply the function
f to get a value of type Option[B]. And whether that result is Some or None
actually depends on the value inside the first Some:
One way to put this difference into words is to say that applicative operations
preserve structure while monadic operations may alter a structure. What does this
mean in concrete terms? Well, for example, if you map over a List with three
elements, the result will always also have three elements. But if you flatMap
over a list with three elements, you may get many more since each function
application can introduce a whole List of new values.
EXERCISE 2: Transplant the implementations of as many combinators as you
can from Monad to Applicative, using only map2, apply, and unit, or
methods implemented in terms of them.
www.it-ebooks.info
212
Now consider what happens in a sequence of flatMaps like this, where each
of the functions validateEmail, validPhone, and validatePostcode
has type Either[String, T] for a given type T:
apply(apply(apply((WebForm(_, _, _)).curried)(
validName(field1)))(
validBirthdate(field2)))(
validPhone(field3))
www.it-ebooks.info
213
This data will likely be collected from the user as strings, and we must make
sure that the data meets a certain specification, or give a list of errors to the user
indicating how to fix the problem. The specification might say that name cannot
be empty, that birthdate must be in the form "yyyy-MM-dd", and that
phoneNumber must contain exactly 10 digits:
www.it-ebooks.info
214
And to validate an entire web form, we can simply lift the WebForm
constructor with apply:
map(v)(x => x) == v
What does this mean for applicative functors? Let's remind ourselves of our
implementation of map:
This law demands that unit preserve identities. Putting the identity function
through unit and then apply results in the identity function itself. But this really
www.it-ebooks.info
215
is just the functor law with an applicative accent, since by our definition above,
map is the same as unit followed by apply.
The second thing that the laws demand of applicative functors is that they
preserve function composition. This is stated as the composition law:
Here, f and g have types like F[A => B] and F[B => C], and x is a value
of type F[A]. Both sides of the == sign are applying g to x and then applying f to
the result of that. In other words they are applying the composition of f and g to x.
All that this law is saying is that these two ways of lifting function composition
should be equivalent. In fact, we can write this more tersely using map2:
map3(f, g, x)(_(_(_)))
This makes it much more obvious what's going on. We're lifting the
higher-order function _(_(_)) which, given the arguments f, g, and x will
return f(g(x)). And the composition law essentially says that even though there
might be different ways to implement map3 for an applicative functor, they should
all be equivalent.
The two remaining applicative laws establish the relationship between unit
and apply. These are the laws of homomorphism and interchange. The
homomorphism law states that passing a function and a value through unit ,
followed by idiomatic application, is the same as passing the result of regular
application through unit:
apply(unit(f))(unit(x)) == unit(f(x))
We know that passing a function to unit and then apply is the same as
simply calling map, so we can restate the homomorphism law:
www.it-ebooks.info
216
map(unit(x))(f) == unit(f(x))
The interchange law completes the picture by saying that unit should have
the same effect whether applied to the first or the second argument of apply:
apply(u)(unit(y)) == apply(unit(_(y)))(u)
The applicative laws are not surprising or profound. Just like the monad laws,
these are simple sanity checks that the applicative functor works in the way that we
would expect. They ensure that apply, unit, map, and map2 behave in a
consistent manner.
EXERCISE 5 (optional, hard): Prove that all monads are applicative functors,
by showing that the applicative laws are implied by the monad laws.
EXERCISE 6: Just like we can take the product of two monoids A and B to give
the monoid (A, B), we can take the product of two applicative functors.
Implement this function:
www.it-ebooks.info
217
Can we generalize this further? Recall that there were a number of data types
other than List that were Foldable in chapter 10. Are there data types other
than List that are traversable? Of course!
EXERCISE 9: On the Applicative trait, implement sequence over a Map
rather than a List:
But traversable data types are too numerous for us to write specialized
sequence and traverse methods for each of them. What we need is a new
interface. We will call it Traverse2:
Footnote 2mThe name Traversable is already taken by an unrelated trait in the Scala standard library.
trait Traverse[F[_]] {
def traverse[M[_]:Applicative,A,B](fa: F[A])(f: A => M[B]): M[F[B]] =
sequence(map(fa)(f))
def sequence[M[_]:Applicative,A](fma: F[M[A]]): M[F[A]] =
traverse(fma)(ma => ma)
}
www.it-ebooks.info
218
At this point you might be asking yourself what the difference is between a
traversal and a fold. Both of them take some data structure and apply a function to
the data within in order to produce a result. The difference is that traverse
preserves the original structure, while foldMap actually throws the structure
away and replaces it with the operations of a monoid.
For example, when traversing a List with an Option-valued function, we
would expect the result to always either be None or to contain a list of the same
length as the input list. This sounds like it might be a law! But we can choose a
simpler applicative functor than Option for our law. Let's choose the simplest
possible one, the identity functor:
type Id[A] = A
Then our law, where xs is an F[A] for some Traverse[F], can be written
like this:
If we replace traverse with map here, then this is just the functor identity
law! This means that in the context of the Id applicative functor, map and
traverse are the same operation. This implies that Traverse can extend
Functor and we can write a default implementation of map in terms of
traverse through Id, and traverse itself can be given a default
implementation in terms of sequence and map:
www.it-ebooks.info
219
Suppose that our M were a type constructor ConstInt that takes any type to
Int, so that ConstInt[A] throws away its type argument A and just gives us
Int:
This looks a lot like foldMap from Foldable. Indeed, if F is something like
List then what we need to implement this signature is a way of combining the
Int values returned by f for each element of the list, and a "starting" value for
handling the empty list. In other words, we only need a Monoid[Int]. And
that's easy to come by.
Indeed, given a constant functor like above, we can turn any Monoid into an
Applicative:
type Const[A, B] = A
www.it-ebooks.info
220
For this reason, applicative functors are sometimes called "monoidal functors".
The operations of a monoid map directly onto the operations of an applicative.
This means that Traverse can extend Foldable and we can give a default
implementation of foldMap in terms of traverse:
www.it-ebooks.info
221
To demonstrate this, here is a State traversal that labels every element with
its position. We keep an integer state, starting with 0, and add 1 at each step:
By the same token, we can keep a state of type List[A], to turn any
traversable functor into a List:
It begins with the empty list Nil as the initial state, and at every element in the
traversal it adds it to the front of the accumulated list. This will of course construct
the list in the reverse order of the traversal, so we end by reversing the list we get
from running the completed state action. Note that we yield () because in this
instance we don't want to return any value other than the state.
Of course, the code for toList and zipWithIndex is nearly identical. And
in fact most traversals with State will follow this exact pattern: We get the
current state, compute the next state, set it, and yield some value. We should
capture that in a function:
www.it-ebooks.info
222
It should obey the following law, for all x and y of the appropriate types:
toList(reverse(x)) ++ toList(reverse(y)) ==
reverse(toList(y) ++ toList(x))
Notice that this version of zip is not able to handle arguments of different
"shapes". For example if F is List then it can't handle lists of different lengths. In
this implementation, the list fb must be at least as long as fa. If F is Tree, then
fb must have at least the same number of branches as fa at every level.
We can change the generic zip slightly and provide two versions so that the
shape of one side or the other is dominant:
www.it-ebooks.info
223
In the case of List for example, the result of zipR will have the shape of the
fb argument, and it will be padded with None on the left if fb is longer than fa.
In the case of Tree, the result of zipR will have the shape of the fb tree, and it
will have Some(a) on the A side only where the shapes of the two trees intersect.
www.it-ebooks.info
224
The flatMap definition here maps over both the M and the Option, and
flattens structures like M[Option[M[Option[A]]]] to just M[Option[A]].
But this particular implementation is specific to Option. And the general strategy
of taking advantage of Traverse works only with traversable functors. To
compose with State for example (which cannot be traversed), a specialized
www.it-ebooks.info
225
12.8 Summary
Applicative functors are a very useful abstraction that is highly modular and
compositional. The functions unit and map allow us to lift functions and values,
while apply and map2 give us the power to lift functions of higher arities. This
in turn enables traverse and sequence to be generalized to traversable
functors. Together, Applicative and Traverse let us construct complex
nested and parallel traversals out of simple elements that need only be written
once.
This is in stark contrast to the situation with monads, where each monad's
composition with others becomes a special case. It's a wonder that monads have
historically received so much attention and applicative functors have received so
little.
Index Terms
applicative functor laws
composition law
homomorphism law
identity law
interchange law
law of composition
law of homomorphism
law of identity
law of interchange
laws of applicative functors
monad transformer
www.it-ebooks.info
226
13.1 Introduction
13
External effects and I/O
In this chapter, we will introduce the I/O monad (usually written 'the IO monad'),
which extends what we've learned so far to handle external effects, like writing to a
file, reading from a database, etc, in a purely functional way. The IO monad will
be important for two reasons:
It provides the most straightforward way of embedding imperative programming into FP,
while preserving referential transparency and keeping pure code separate from what we'll
call effectful code. We will be making an important distinction here in this chapter
between effects and side effects.
It illustrates a key technique for dealing with external effects—using pure functions to
compute a description of an imperative computation, which is then executed by a
separate interpreter. Essentially, we are crafting an embedded domain specific language
(EDSL) for imperative programming. This is a powerful technique we'll be using
throughout part 4; part of our goal is to equip you with the skills needed to begin crafting
your own descriptions for interesting effectful programs you are faced with.
www.it-ebooks.info
227
In chapter 1, we factored out the logic for computing the winner into a function
separate from displaying the winner:
These might seem like silly examples, but the same principles apply in larger,
more complex programs and we hope you can see how this sort of factoring is
quite natural. We aren't changing what our program does, just the internal details of
how it is factored into smaller functions. The insight here is that inside every
function with side effects is a pure function waiting to get out. We can even
formalize this a bit. Given an impure function of type A => B, we can often split
this into two functions:1
Footnote 1mWe will see many more examples of this in this chapter and in the rest of part 4.
www.it-ebooks.info
228
We will extend this to handle 'input' side effects shortly. For now, though,
consider applying this strategy repeatedly to a program. Each time we apply it, we
make more functions pure and push side effects to the outer layers of the program.
We sometimes call these impure functions the 'imperative shell' around the pure
core of the program. Eventually, we reach functions that seem to necessitate side
effects like the built-in println, which has type String => Unit. What do
we do then?
www.it-ebooks.info
229
}
}
object IO {
def empty: IO = new IO { def run = () }
}
The only thing we can say about IO as it stands right now is that it forms a
Monoid (empty is the identity, and ++ is the associative operation). So if we
have for instance a List[IO], we can reduce that to an IO, and the associativity
of ++ means we can do this by folding left or folding right. On its own, this isn't
very interesting. All it seems to have given us is the ability to delay when a side
effect gets 'paid for'.
Now we will let you in on a secret—you, as the programmer, get to invent
whatever API you wish to represent your computations, including those that
interact with the universe external to your program. This process of crafting
pleasing, useful, and composable descriptions of what you want your programs to
do is at its core language design. You are crafting a little language and an
associated interpreter that will allow you to express various programs. If you don't
like something about this language you have created, change it! You should
approach this task just like any other combinator library design task, and by now
you've had plenty of experience designing and writing such libraries.
www.it-ebooks.info
230
def converter: IO = {
val prompt: IO = PrintLine(
"Enter a temperature in degrees fahrenheit: ")
// now what ???
}
syntax for IO ..
www.it-ebooks.info
231
Here's a larger example, an interactive program which prompts the user for
input in a loop, then computes the factorial of the input. Here's an example run:
This code uses a few Monad functions we haven't seen yet, when, foreachM,
www.it-ebooks.info
232
and sequence_, discussed in the sidebar below. For the full listing, see the
associated chapter code. The details of this code aren't too important; the point here
is just to demonstrate how we can embed an imperative programming language
into the purely functional subset of Scala. All the usual imperative programming
tools are here—we can write loops, perform I/O, and so on. If you squint, it looks a
bit like normal imperative code.
www.it-ebooks.info
233
We don't necessarily endorse writing code this way.4 What this does
demonstrate, however, is that FP is not in any way limited in its
expressiveness—any program that can be expressed can be expressed in FP, even
if that functional program is a straightforward embedding of the imperative
program into the IO monad.
Footnote 4mIf you have a monolithic block of impure code like this, you can always just write a definition
which performs actual side effects then wrap it in IO—this will be more efficient, and the syntax is nicer than
what is provided using a combination of for-comprehension syntax and the various Monad combinators.
www.it-ebooks.info
234
IO computations are ordinary values. We can store them in lists, pass them to functions,
create them dynamically, etc. Any common pattern we notice can be wrapped up in a
function and reused. This is one reason why it is sometimes argued that functional
languages provide more powerful tools for imperative programming, as compared to
languages like C or Java.
Reifying IO computations as values means we can craft a more interesting interpreter
than the simple run-based "interpreter" baked into the IO type itself. Later on in this
chapter, we will build a more refined IO type and sketch out an interpreter that uses
nonblocking I/O in its implementation. Interestingly, client code like our converter
example remains identical—we do not expose callbacks to the programmer at all! They
are entirely an implementation detail of our IO interpreter.
www.it-ebooks.info
235
Aside from the fact that IO is non-strict, and that retrieving the value is done
using run instead of value, the types are identical! Our "IO" type is just a
non-strict value. This is rather unsatisfying. What's going on here?
We have actually cheated a bit with our IO type. We are relying on the fact that
Scala allows unrestricted side effects at any point in our programs. But let's think
about what happens when we evaluate the run function of an IO. During
evaluation of run, the pure part of our program will occasionally make requests of
the outside world (like when it invokes readLine), wait for a result, and then
pass this result to some further pure computation (which may subsequently make
some further requests of the outside world). Our current IO type is completely
inexplicit about where these interactions are occurring. But we can model these
interactions more explicitly if we choose:
trait IO[+A]
case class Pure[+A](a: A) extends IO[A]
case class Request[I,+A](expr: External[I],
receive: I => IO[A]) extends IO[A]
This type separates the pure and effectful parts of an IO computation. We'll get
to writing its Monad instance shortly. An IO[A] can be a pure value, or it can be
a request of the external computation. The type External defines the protocol
www.it-ebooks.info
236
—it encodes what possible external requests our program can make. We can think
of External[I] much like an expression of type I, but it is an expression that is
"external" that must be evaluated by whatever program is running this IO action.
The receive function defines what to do when the result of the request becomes
available, it is sometimes called the continuation. We'll see a bit later how this can
be exploited to write an interpreter for IO that uses nonblocking I/O internally.
The simplest possible representation of External[I] would be simply a
nonstrict value:6
Footnote 6mSimilar to our old IO type!
This implies that our IO type can call absolutely any impure Scala function,
since we can wrap any expression at all in Delay (for instance, Delay {
println("Side effect!") }). If we want to restrict access to only certain
functions, we can parameterize our IO type on the choice of External.7
Footnote 7mOf course, Scala will not technically prevent us from invoking a function with side effects at any
point in our program. This discussion assumes we are following the discipline of not allowing side effects unless
this information is tracked in the type.
trait Console[A]
case object ReadLine extends Console[Option[String]]
case class PrintLine(s: String) extends Console[Unit]
www.it-ebooks.info
237
system, a network F granting the ability to open network connections and read
from them, and so on. Notice, interestingly, that nothing about Console implies
that any side effects must actually occur! That is a property of the interpreter of F
values now required to actually run an IO[F,A]:
Footnote 8m
trait Run[F[_]] {
def apply[A](expr: F[A]): (A, Run[F])
}
object IO {
@annotation.tailrec
def run[F[_],A](R: Run[F])(io: IO[F,A]): A = io match {
case Pure(a) => a
case Request(expr,recv) =>
R(expr) match { case (e,r2) => run(r2)(recv(e)) }
}
}
Ignored!
www.it-ebooks.info
238
EXERCISE 1: Give the Monad instance for IO[F,_].9 You may want to
override the default implementations of various functions with more efficient
versions.
Footnote 9mNote we must use the same trick discussed in chapter 10, to partially apply the IO type
constructor.
www.it-ebooks.info
239
object IO {
...
def apply[A](a: => A): IO[Runnable,A] =
Request(Delay(a), (a:A) => Pure(a))
}
www.it-ebooks.info
240
Here are some perfectly reasonable IO programs that can present problems:
trait Trampoline[+A]
case class Done[+A](get: A) extends Trampoline[A]
case class More[+A](force: () => Trampoline[A]) extends Trampoline[A]
case class Bind[A,+B](force: () => Trampoline[A],
f: A => Trampoline[B]) extends Trampoline[B]
@annotation.tailrec
def run[A](t: Trampoline[A]): A
www.it-ebooks.info
241
We can now add the same trampolining behavior directly to our IO type:
www.it-ebooks.info
242
We have a More constructor, as before, but we now have two additional Bind
constructors.
EXERCISE 8: Implement both versions of run for this new version of IO.
EXERCISE 9 (hard): Implement Monad for this new version of IO. Once
again, a correct implementation of flatMap will not invoke run or force().
Test your implementation using the examples we gave above. Can you see why it
works? Again, you may want to try tracing their execution until the pattern
becomes clear, or even construct a proof that stack usage is guaranteed to be
bounded.
www.it-ebooks.info
243
We can do better. There are I/O libraries that support nonblocking I/O. The
details of these libraries vary, but to give the general idea, a nonblocking source of
bytes might have an interface like this:
trait Source {
def requestBytes(
nBytes: Int,
callback: Either[Throwable, Array[Byte]] => Unit): Unit
}
One of the nice things about making I/O computations into values is that we
can build more interesting interpreters for these values than the default 'interpreter'
which performs I/O actions directly. Here we will sketch out an interpreter that
uses nonblocking I/O internally. The ugly details of this interpreter are completely
hidden from code that uses the IO type, and code that uses IO can be written in a
much more natural style without explicit callbacks.
Recall that earlier, we implemented a function with the following signature:
We are going to simply choose Future for our F, though we will be using a
different definition than the java.util.concurrent.Future introduced in
chapter 7. The version we use here can be backed by a nonblocking I/O primitive.
It is trampolined, as we've seen before.16
www.it-ebooks.info
244
Footnote 16mThis definition can be extended to handle timeouts, cancellation, and deregistering callbacks.
We won't be discussing these extensions here, but you may be interested to explore this on your own. See the
Task.scala for an extension to Future supporting proper error handling.
trait Future[+A]
object Future {
case class Now[+A](get: A) extends Future[A]
case class Later[+A](listen: (A => Unit) => Unit) extends Future[A]
case class More[+A](force: () => Future[A]) extends Future[A]
case class BindLater[A,+B](listen: (A => Unit) => Unit,
f: A => Future[B]) extends Future[B]
case class BindMore[A,+B](force: () => Future[A],
f: A => Future[B]) extends Future[B]
}
Future looks almost identical to IO, except we have replaced the Request
and BindRequest cases with Later and BindLater constructors. The
listen function of Later is presumed to have some side effect—perhaps
adding callback to a mutable list of listeners to notify when the result becomes
available. We will only be using it here as an implementation detail of the run
function for IO (this could be enforced using access modifiers).
EXERCISE 10 (hard, optional): Implement Monad[Future]. We will need it
to implement our nonblocking IO interpreter. Also implement runAsync, for an
asynchronous evaluator for Future, and run, the synchronous evaluator:
www.it-ebooks.info
245
With this in place, we can now write the following to asynchronously evaluate
an IO[Console,A]:
That's it! In general, for any F type, we just require a way to translate that F to a
Future and we can then evaluate IO[F,A] programs asynchronously. Let's look
at some possible definitions of RunConsole:
First, if we wish, we can always implement RunConsole using ordinary
blocking I/O calls:17
Footnote 17mRecall that Future.unit doesn't do anything special with its argument—the argument will
still be evaluated in the main thread using ordinary function calls.
www.it-ebooks.info
246
object Future {
...
def apply[A](a: => A): Future[A] = { ... }
}
EXERCISE 12 (hard, optional): Going one step further, we can use an API that
directly supports nonblocking I/O. We are not going to work through such an
implementation here, but you may be interested to explore this on your own by
building off the java.nio library (API link. As a start, try implementing an
asynchronous read from an AsynchronousFileChannel (API link).18
Footnote 18mThis requires Java 7.
www.it-ebooks.info
247
trait Files[A]
case class ReadLines(file: String) extends Files[List[String]]
case class WriteLines(file: String, lines: List[String])
extends Files[Unit]
for {
lines <- request(ReadLines("fahrenheit.txt"))
cs = lines.map(s => fahrenheitToCelsius(s.toDouble).toString)
_ <- request(WriteLines("celsius.txt", cs))
} yield ()
trait Files[A]
case class OpenRead(file: String) extends Files[HandleR]
case class OpenWrite(file: String) extends Files[HandleW]
case class ReadLine(h: HandleR) extends Files[Option[String]]
case class WriteLine(h: HandleW, line: String) extends Files[Unit]
trait HandleR
trait HandleW
www.it-ebooks.info
248
The only problem with this is we now need to write a monolithic loop:
There's nothing inherently wrong with writing a monolithic loop like this, but
it's not composable. Suppose we decide later that we'd like to compute a 5-element
moving average of the temperatures. Modifying our loop function to do this
would be somewhat painful. Compare that to the equivalent change we'd make to
our List-based code, where we could define a movingAvg function and just
stick it before or after our conversion to celsius:
The point to all this is that programming with a composable abstraction like
List is much nicer than programming directly with the primitive I/O operations.
Lists are not really special—they are just one instance of a composable API that is
pleasant to use. We should not have to give up all the nice compositionality we've
www.it-ebooks.info
249
come to expect from FP just to write programs that make use of efficient,
streaming I/O.21 Luckily we don't have to. As we will see in chapter 15, we get to
build whatever abstractions we want for creating computations that perform I/O. If
we like the metaphor of lists or streams, we can find a way to encode a list-like
API for expressing I/O computations. If we invent or discover some other
composable abstraction, we can often find some way of using that.
Footnote 21mOne might ask—could we just have various Files operations return the Stream type we
defined in chapter 5? This is called lazy I/O, and it is problematic for several reasons we'll discuss more in
chapter 15.
13.7 Conclusion
This chapter introduced the simplest model for how external effects and I/O can be
handled in a purely functional way. We began with a discussion of effect-factoring
and demonstrated how effects can be moved to the outer layers of a program. From
there, we defined two IO data types, one with a simple interpreter built into the
type, and another which made interactions with the external universe more explicit.
This second representation allowed us to implement a more interesting interpreter
of IO values that used nonblocking I/O internally.
The IO monad is not the final word in writing effectful programs. It is
important because it represents a kind of lowest common denominator. We don't
normally want to program with IO directly, and in chapter 15 we will discuss how
to build nicer, more composable abstractions.
Before getting to that, in our next chapter, we will apply what we've learned so
far to fill in the other missing piece of the puzzle: local effects. At various places
throughout this book, we've made use of local mutation rather casually, with the
assumption that these effects were not observable. Next chapter we explore what
this means in more detail, show more example usages of local effects, and show
how effect scoping can be enforced by the type system.
Index Terms
..
continuation
effect scoping
trampolining
unobservable
www.it-ebooks.info
250
14.1 Introduction
14
Local effects and mutable state
www.it-ebooks.info
251
By that definition, the following function is pure, even though it uses a while
loop, an updatable var, and a mutable array:
The quicksort function sorts a list by turning it into a mutable array, sorting
the array in place using the well-known Quicksort algorithm, and then turning the
array back into a list. It's not possible for any caller to know that the individual
subexpressions inside the body of quicksort are not referentially transparent or
that the local methods swap, partition, and qs are not pure, because at no
point does any code outside the quicksort function hold a reference to the
mutable array. Since all of the mutation is locally scoped, the overall function is
www.it-ebooks.info
252
pure. That is, for any referentially transparent expression xs of type List[Int],
the expression quicksort(xs) is also referentially transparent.
Some algorithms, like Quicksort, need to mutate data in place in order to work
correctly or efficiently. Fortunately for us, we can always safely mutate data that is
created locally. Any function can use side-effecting components internally and still
present a pure external interface to its callers.
A mutable object can never be observed outside of the scope in which it was created.
If we hold a reference to a mutable object, then nothing can observe us mutating it.
We will call this new local effects monad ST, which could stand for "State
Thread", "State Transition", "State Token", or "State Tag". It's different from the
State monad in that its run method is protected, but otherwise its structure is
exactly the same.
www.it-ebooks.info
253
The reason the run method is protected is that an S represents the ability to
mutate state, and we don't want the mutation to escape. So how do we then run an
ST action, giving it an initial state? These are really two questions. We will start by
answering the question of how we specify the initial state.
As always, don't feel compelled to understand every detail of the
implementation of ST. What matters is the idea that we can use the type system to
constrain the scope of mutable state.
The data structure we'll use for mutable references is just a wrapper around a
protected var:
www.it-ebooks.info
254
object STRef {
def apply[S,A](a: A): ST[S, STRef[S,A]] = ST(new STRef[S,A] {
var cell = a
})
}
The methods on STRef to read and write the cell are pure since they just return
ST actions. Notice that the type S is not the type of the cell that's being mutated,
and we never actually use the value of type S. Nevertheless, in order to call apply
and actually run one of these ST actions, you do need to have a value of type S.
That value therefore serves as a kind of token—an authorization to mutate or
access the cell, but it serves no other purpose.
The question of how to give an initial state is answered by the apply method
on the STRef companion object. The STRef is constructed with an initial value
for the cell, of type A. But what is returned is not a naked STRef, but an ST action
that constructs the STRef when run. That is, when given the token of type S.
At this point, let's try writing a trivial ST program. It's a little awkward right
now because we have to choose a type S arbitrarily. Here, we arbitrarily choose
Nothing:
for {
r1 <- STRef[Nothing,Int](1)
r2 <- STRef[Nothing,Int](1)
x <- r1.read
y <- r2.read
_ <- r1.write(y+1)
_ <- r2.write(x+1)
a <- r1.read
b <- r2.read
} yield (a,b)
This little program allocates two mutable Int cells, swaps their contents, adds
www.it-ebooks.info
255
one to both, and then reads their new values. But we can't yet run this program
because run is still protected (and we could never actually pass it a value of type
Nothing anyway). Let's work on that.
The former is an ST action that contains a mutable reference. But the latter is
quite different. A value of type ST[S,Int] is quite literally just an Int, even
though computing the Int may involve some local mutable state. Fortunately for
us, there's an exploitable difference between these two types. The STRef involves
the type S, but Int does not.
We want to disallow running an action of type ST[S, STRef[S,A]]
because that would expose the STRef. And in general we want to disallow
running any ST[S,T] where T involves the type S. On the other hand, it's easy to
see that it should always be safe to run an ST action that doesn't expose a mutable
object. If we have such a pure action of a type like ST[S,Int], it should be safe
to pass it an S to get the Int out of it. Furthermore, we don't care what S actually
is in that case because we are going to throw it away. The value might as well be
polymorphic in S.
In order to represent this, we will introduce a new trait that represents ST
actions that are safe to run. In other words, actions that are polymorphic in S:
www.it-ebooks.info
256
trait RunnableST[A] {
def apply[S]: ST[S,A]
}
This is very similar to the idea behind the Trans trait from the previous
chapter. A value of type RunnableST[A] is a function that takes a type S and
produces a value of type ST[S,A].
In the section above we arbitrarily chose Nothing as our S type. Let's instead
wrap it in RunnableST making it polymorphic in S. Then we do not have to
choose the type S at all. It will be supplied by whatever calls apply.
We are now ready to write the runST function that will call apply on any
polymorphic RunnableST by arbitrarily choosing a type for S. Since the
RunnableST action is polymorphic in S, it's guaranteed to not make use of the
value that gets passed in. So it's actually completely safe to pass null!
The runST function must go on the ST companion object. Since run is
protected on the ST trait, it's accessible from the companion object but nowhere
else:
object ST {
def apply[S,A](a: => A) = {
lazy val memo = a
new ST[S,A] {
def run(s: S) = (memo, s)
}
}
def runST[A](st: RunnableST[A]): A =
st[Null].run(null)._1
}
www.it-ebooks.info
257
The expression runST(p) uses mutable state internally but it does not have
any side-effects. As far as any other expression is concerned, it's just a pair of
integers like any other. It will always return the same pair of integers and it will do
nothing else.
But this is not the most important part. Most importantly, we cannot run a
program that tries to return a mutable reference. It's not possible to create a
RunnableST that returns a naked STRef.
In this example, we arbitrarily choose Nothing just to illustrate the point. The
point is that the type S is bound in the apply method, so when we say new
RunnableST, that type is not accessible.
Because an STRef is always tagged with the type S of the ST action that it
lives in, it can never escape. And this is guaranteed by Scala's type system! As a
corollary, the fact that you cannot get an STRef out of an ST action guarantees
that if you have an STRef then you are inside of the ST action that created it, so
it's always safe to mutate the reference.
www.it-ebooks.info
258
This type error is caused by the fact that the wildcard type in ref
represents some concrete type that only ref knows about. In this case
it's the S type that was bound in the apply method of the RunnableST
where it was created. Scala is unable to prove that this is the same type
as R. Therefore, even though it's possible to abuse the wildcard type to
get the naked STRef out, this is still safe since we can't use it to mutate
or access the state.
www.it-ebooks.info
259
object STArray {
def apply[S,A:Manifest](sz: Int, v: A): ST[S, STArray[S,A]] =
new STArray[S,A] {
lazy val value = Array.fill(sz)(v)
}
}
A thing to note is that Scala cannot create arrays for every type. It requires that
there exist a Manifest for the type in implicit scope. Scala's standard library
provides manifests for most types that you would in practice want to put in an
array. The implicitly function simply gets that manifest out of implicit scope.
Just like with STRefs, we always return STArrays packaged in an ST action
with a corresponding S type, and any manipulation of the array (even reading it), is
an ST action tagged with the same type S. It's therefore impossible to observe a
naked STArray outside of the ST monad (except in the Scala source file in which
the STArray data type itself is declared).
Using these primitives, we can write more complex functions on arrays.
EXERCISE 1: Add a combinator on STArray to fill the array from a Map
where each key in the map represents an index into the array, and the value under
that key is written to the array at that index. For example, fill(Map(0->"a",
2->"b")) should write the value "a" at index 0 in the array and "b" at index 2.
Use the existing combinators to write your implementation.
www.it-ebooks.info
260
Not everything can be done efficiently using these existing combinators. For
example, the Scala library already has an efficient way of turning a list into an
array. Let's make that primitive as well:
With those components written, quicksort can now be assembled out of them in
the ST monad:
www.it-ebooks.info
261
As you can see, the ST monad allows us to write pure functions that
nevertheless mutate the data they receive. Scala's type system ensures that we don't
combine things in an unsafe way.
EXERCISE 3: Give the same treatment to
scala.collection.mutable.HashMap as we have given here to
references and arrays. Come up with a minimal set of primitive combinators for
creating and manipulating hash maps.
scala> val d = x eq x
d: Boolean = true
www.it-ebooks.info
262
This definition is only slightly modified to reflect the fact that not all programs
can observe the same effects. We say that an effect of e is non-observable by p if
it doesn't affect the referential transparency of e with regard to p.
We should also note that this definition makes some assumptions. What is
meant by "evaluating"? And what is the standard by which we determine whether
the results of two evaluations are the same?
In Scala, there is a kind of standard answer to these questions. Generally,
evaluation means reduction to some normal form. Since Scala is a strictly
evaluated language, we can force the evaluation of an expression e to normal form
in Scala by assigning it to a val:
val v = e
www.it-ebooks.info
263
If you replace timesTwo(1) with 2 in your program, you do not have the
same program in every respect. It may compute the same result, but we can say
that the observable behavior of the program has changed. But this is not true for all
programs that call timesTwo, nor for all notions of program equivalence.
We need to decide up front whether changes in standard output are something
we care to observe—whether it's part of the changes in behavior that matter in our
context. In this case it's exceedingly unlikely that any other part of the program
will be able to observe that println side-effect occurring inside timesTwo.
Of course, timesTwo has a hidden dependency on the I/O subsystem. It
requires access to the standard output stream. But as we have seen above, most
programs that we would consider purely functional also require access to some of
the underlying machinery of Scala's environment, like being able to construct
objects in memory and discard them. At the end of the day, we have to decide for
ourselves which effects are important enough to track. We could use the IO monad
to track println calls, but maybe we don't want to bother. If we're just using the
www.it-ebooks.info
264
console to do some temporary debug logging, it seems like a waste of time to track
that. But if the program's correct behavior depends in some way on the what it
prints to the console (like if it's a UNIX utility), then we definitely want to track it.
This brings us to an essential point: Keeping track of effects is a choice we
make as programmers. It's a value judgement, and there are trade-offs associated
with how we choose. We can take it as far as we want. But as with the context of
referential transparency, in Scala there is a kind of standard choice. For example it
would be completely valid and possible to track memory allocations in the type
system if that really mattered to us. But in Scala we have the benefit of automatic
memory management so the cost of explicit tracking is usually higher than the
benefit.
The policy we should adopt is to track those effects that program correctness
depends on. If a program is fundamentally about reading and writing files, then file
I/O should be tracked in the type system to the extent feasible. If a program relies
on object reference equality, it would be nice to know that statically as well. Static
type information lets us know what kinds of effects are involved, and thereby lets
us make educated decisions about whether they matter to us in a given context.
The ST type in this chapter and the IO monad in the previous chapter should
have given you a taste for what it's like to track effects in the type system. But this
is not the end of the road. You're limited only by your imagination and the
expressiveness of Scala's types.
14.4 Summary
In this chapter, we discussed two different implications of referential transparency.
We saw that we can get away with mutating data that never escapes a local
scope. At first blush it may seem that mutating state can't be compatible with pure
functions. But as we have seen, we can write components that have a pure interface
and mutate local state behind the scenes, using Scala's type system to guarantee
purity.
We also discussed that what counts as a side-effect is actually a choice made by
the programmer or language designer. When we talk about functions being pure,
we should have already chosen a context that establishes what it means for two
things to be equal, what it means to execute a program, and which effects we care
to take into account when observing the program's behavior.
www.it-ebooks.info
265
Index Terms
extensional equality
extensionality
intensional equality
intensionality
www.it-ebooks.info
266
15.1 Introduction
15
We said in the introduction to part 4 that functional programming is a complete
paradigm. Any program that can be imagined can be expressed functionally,
including those that interact with the external world. But it would be disappointing
if the IO type were our only way of constructing such programs. IO (and ST) work
by simply embedding an imperative programming language into the purely
functional subset of Scala. While programming within the IO monad, we have to
reason about our programs much like we would in ordinary imperative
programming.
We can do better. Not only can functional programs embed arbitrary imperative
programs; in this chapter we show how to recover the high-level, compositional
style developed in parts 1-3 of this book, even for programs that interact with the
outside world. The design space in this area is enormous, and our goal here is more
to convey ideas and give a sense of what is possible.1
Footnote 1mAs always, there is more discussion and links to further reading in the chapter notes.
www.it-ebooks.info
267
accomplish this task with ordinary imperative code, inside the IO monad. Let's
look at that first:2
Footnote 2mFor simplicity, in this chapter we are not going to parameterize our IO type on the F language
used. That is, let's assume that type IO[A] = fpinscala.iomonad.IO[Task,A], where Task[A]
just wraps a Future[Either[Throwable,A]] with some functions for error handling. This should be
taken to mean that within an IO[A] we can make use of any impure Scala function. See the chapter code for
details.
Although this code is rather low-level, there are a number of good things about
it. First, it is incremental—the entire file is not loaded into memory up front.
Instead, lines are fetched from the file only when needed. If we didn't buffer our
input, we could keep as little as a single line of the file in memory at a time. It also
terminates early, as soon as the answer is known, rather than reading the entire file
and then returning an answer.
There are some bad things about this code, too. For one, we have to remember
to close the file when we're done. This might seem obvious, but if we forget to do
this or (more commonly) if we close the file outside of a finally block and an
exception occurs first, the file will remain open.3 This is called a resource leak. A
file handle is an example of a scarce resource—the operating system can only have
a limited number of files open at any given time. If this task were part of a larger
www.it-ebooks.info
268
We want to write programs that are resource safe—that is, they should close
file handles as soon as they are finished with them (whether because of normal
termination or an exception), and they should not attempt to read from a closed
file. Likewise for other resources like network connections, database connections,
and so on. Using IO directly can be problematic because it means our programs
are entirely responsible for ensuring resource safety, and we get no help from the
compiler in making sure of this. It would be nice if our library would ensure
resource safety by construction.
But even aside from the problems with resource safety, there is something
rather low-level and unsatisfying about this code. We should be able to express the
algorithm—of counting elements and stopping with a response as soon as we hit
40,000, independent of how we are to obtain these elements. Opening and closing
files and catching exceptions is a separate concern from the fundamental algorithm
being expressed, but this code intertwines these concerns. This isn't just ugly, it's
not compositional, and our code will be difficult to extend later. For instance,
consider a few variations of this scenario:
Check whether the number of nonempty lines in the file exceeds 40,000
www.it-ebooks.info
269
Find a line index before 40,000 where the first letter of consecutive lines spells out
"abracadabra".
For this first case, we could imagine passing a String => Boolean into
our linesGt40k function. But for the second case, we would need to modify our
loop to keep track of some further state, and besides being uglier, the resulting
code will likely be tricky to get right. In general, writing efficient code in the IO
monad generally means writing monolithic loops, and monolithic loops are not
composable.
Let's compare this to the case where we have a Stream[String] for the
lines being analyzed.
Much nicer! With a Stream, we get to assemble our program from preexisting
combinators, zipWithIndex and exists. If we want to filter these lines, we
can do so easily:
And for the second scenario, we can use the indexOfSlice function defined
on Stream,4 in conjunction with take (to terminate the search after 40,000 lines)
and map (to pull out the first character of each line):
Footnote 4mIf the argument to indexOfSlice does not exist as a subsequence of the input, -1 is returned.
See the API docs for details, or experiment with this function in the REPL.
lines.take(40000).map(_.head).indexOfSlice("abracadabra".toList)
A natural question to ask is, could we just write the above programs if reading
from an actual file? Not quite. The problem is we don't have a
Stream[String], we have a file from which we can read a line at a time. We
could cheat by writing a function, lines , which returns an
IO[Stream[String]]:
www.it-ebooks.info
270
This is called lazy I/O. We are cheating because the Stream[String] inside
the IO is not actually a pure value. As elements of the stream are forced, it will
execute side effects of reading from the file, and only if we examine the entire
stream and reach its end will we close the file. Although it is appealing that lazy
I/O lets us recover the compositional style to some extent, it is problematic for
several reasons:
It isn't resource safe. The resource (in this case, a file) will be released only if we traverse
to the end of the stream. But we will frequently want to terminate traversal early (here,
exists will stop traversing the Stream as soon as it finds a match) and we certainly don't
want to leak resources every time we do this.
Nothing stops us from traversing that same Stream again, after the file has been closed,
resulting in either excessive memory usage (if the Stream is one that caches or memoizes
its values once forced) or an error if the Stream is unmemoized and this causes a read
from a closed file handle. Also, having two threads traverse an unmemoized Stream at
the same time can result in unpredictable behavior.
In more realistic scenarios, we won't necessarily have full knowledge of what is
happening with the Stream[String] we created. It could be passed on to some other
function we don't control, which might store it in a data structure for a long period of
time before ever examining it, etc. Proper usage now requires some out-of-band
knowledge—we cannot necessarily just manipulate this Stream[String] like a typical
pure value, we have to know something about its origin. This is bad for the compositional
style typically used in FP, where most of our code won't know anything about a value
other than its type.
Lazy I/O is problematic, but it would be nice to recover the high-level style we
are accustomed to from our usage of Stream and List. In the next section, we'll
introduce the notion of stream transducers or stream processors, which is our first
step toward achieving this.
www.it-ebooks.info
271
Footnote 5mWe have chosen to omit variance annotations in this chapter for simplicity, but it is possible to
write this as Process[-I,+O].
trait Process[I,O]
case class Emit[I,O](
head: Seq[O],
tail: Process[I,O] = Halt[I,O]())
extends Process[I,O]
case class Await[I,O](
recv: I => Process[I,O],
finalizer: Process[I,O] = Halt[I,O]())
extends Process[I,O]
case class Halt[I,O]() extends Process[I,O]
Emit(head,tail) indicates to the driver that the head values should be emitted to the
output stream, and that tail should be the next state following that.6
Footnote 6mWe could choose to have Emit produce just a single value. The use of Seq avoids stack
overflows for certain Process definitions.
Await(recv,finalizer) requests a value from the input stream, indicating that recv
should be used by the driver to produce the next state, and that finalizer should be
consulted if the input has no more elements available.
Halt indicates to the driver that no more elements should be read from the input stream
or emitted to the output.
Let's look at a sample driver that will actually interpret these requests. Here is
one that actually transforms a Stream. We can implement this as a function on
Process:
www.it-ebooks.info
272
The implementation simply calls map on any values produced by the Process
. As with lists, we can also append processes. Given two processes, x and y, x ++
y runs x to completion, then runs y to completion on whatever input remains after
the first has halted. For the implementation, we simply replace the Halt of x with
y (much like how ++ on List replaces the Nil of the first list with the second
list):
This uses a helper function, emitAll, which behaves just like the Emit
constructor but combines adjacent emit states into a single Emit. For instance,
emitAll(h1, Emit(h2, t)) becomes Emit(h1 ++ h2, t). (A
www.it-ebooks.info
273
function like this that just calls some constructor of a data type but enforces some
addition invariant is often called a smart constructor.)
def emit[I,O](head: O,
tail: Process[I,O] = Halt[I,O]()): Process[I,O] =
emitAll(Stream(head), tail)
Incidentally, Process forms a Monad. The unit function just emits a single
value, then halts:
www.it-ebooks.info
274
The Monad instance is exactly the same 'idea' as the Monad for List. What
makes Process more interesting than just List is it can accept input. And it can
transform that input through mapping, filtering, folding, grouping, and so on. It
turns out that Process can express almost any stream transformation, all while
remaining agnostic to how exactly it is obtaining its input or what should happen
with its output.
PROCESS COMPOSITION, LIFTING, AND REPETITION
The way we will build up complex stream transformations is by composing
Process values. Given two Process values, f and g, we can feed the output f
into the input of g. We'll call this operation |> (pronounced 'pipe' or 'compose')
and implement it as a function on Process.8 It has the nice property that f |>
g fuses transformations of f and g. As soon as values are emitted by f, they are
transformed by g.
Footnote 8mThis operation might remind you of function composition, which feeds the (single) output of a
function in as the (single) input to another function. Both Process and functions form categories. We won't
be discussing that much here, but see the chapter notes.
EXERCISE 1 (hard): Implement |>. Let the types guide your implementation.
www.it-ebooks.info
275
This pattern is quite common—we often have some Process whose steps we
wish to repeat forever. We can write a combinator for it, repeat:
Again, we use the same pattern of an inner function which tracks the current
state (in this case, the total so far). Here's an example of its use in the REPL:
www.it-ebooks.info
276
Let's get write some more Process combinators to get accustomed to this
style of programming. Try to work through implementations of at least some of
these exercises until you get the hang of it.
EXERCISE 2: Implement take, which halts the Process after it encounters
the given number of elements, and drop, which ignores the given number of
arguments, then emits the rest. Optional: implement takeWhile and
dropWhile.
Just as we have seen many times before throughout this book, when we notice
common patterns when defining a series of functions, we can factor these patterns
out into generic combinators. The functions sum, count and mean all share a
www.it-ebooks.info
277
common pattern. Each has a single piece of state, and a state transition function
that updates this state in response to input and produces a single output. We can
generalize this to a combinator, loop:
We can now express the core stream transducer for our line-counting problem
as count |> exists(_ > 40000). Of course, it's easy to attach filters and
other transformations to our pipeline.
www.it-ebooks.info
278
trait Source[O] {
def |>[O2](p: Process[O,O2]): Source[O2]
def filter(f: O => Boolean) = this |> Process.filter(f)
def map[O2](f: O => O2) = this |> Process.lift(f)
}
case class ResourceR[R,I,O]( // A resource from which we can read values
acquire: IO[R],
release: R => IO[Unit],
step: R => IO[Option[I]],
trans: Process[I,O]) extends Source[O] {
def |>[O2](p: Process[O,O2]) =
ResourceR(acquire, release, step, trans |> p)
}
@annotation.tailrec
def go(acc: IndexedSeq[O],
step: IO[Option[I]],
p: Process[I,O],
release: IO[Unit]): IndexedSeq[O] =
p match {
case Halt() => release.run; acc
case Emit(h, t) =>
go(tryOr(acc ++ h)(release), step, t, release)
case Await(recv, fb) => tryOr(step.run)(release) match {
case None => go(acc, IO(None), fb, release)
www.it-ebooks.info
279
Our code for checking whether the number of lines in a file exceeds 40,0000
now looks like Source.lines("input.txt").count.exists(_ >
40000). This is nicely compositional, and we are assured that calling collect
on this Source will open the file and guarantee it is closed, regardless of whether
exceptions occur. We deal with resource safety in just two places, the collect
function we wrote earlier, and the definition of lines—the knowledge of how to
allocate and release a resource is encapsulated in a single type, Source, and
collect is the sole driver that must take care to use this information to ensure
www.it-ebooks.info
280
resource safety. This is in contrast to ordinary imperative I/O (in the IO monad or
otherwise) where any code that reads from files must repeat the same (error-prone)
patterns to ensure resource safety.
Although we can get quite far with Process and Source, and the simple
way we have combined them here is resource safe, these data types are too simple
to express a number of interesting and important use cases. Let's look at one of
those next:
We'd like to write a program that reads this and produces celsius.txt:
29.5556
28.38889
26.6667
22.16667
...
Our program should work in a streaming fashion, emitting to the output file as
lines are read from the input file, while staying resource safe. With the library we
have so far, we can certainly produce a Source[Double] containing the
temperatures we need to output to celsius.txt:
www.it-ebooks.info
281
Unfortunately, Source lacks the ability to actually write these lines to the
output file. In general, one way we can handle these expressiveness problems is by
adding extra cases to Source. Here, we could try solving our immediate problem
by first introducing a new type, Sink, analogous to Source:
trait Sink[I] {
def <|[I0](p: Process[I0,I]): Sink[I0]
def filter(f: I => Boolean) = this <| Process.filter(f)
...
}
case class ResourceW[R,I,I2](
acquire: IO[R],
release: R => IO[Unit],
recv: R => (I2 => IO[Unit]),
trans: Process[I,I2]) extends Sink[I] {
How might we integrate this into our Source API? Let's imagine a new
combinator, observe:
www.it-ebooks.info
282
This uses the helper function run, which ignores the output of a Source,
evaluating it only for its effects. See the chapter code for its implementation.
Ultimately, this approach of adding special cases to Source starts getting
rather ugly. Let's take a step back and consider some additional scenarios for which
our existing API is insufficient. These are just informal descriptions.
Multi-source input/zipping: 'Zip' together two files, f1.txt and f2.txt, each containing
temperatures in degrees fahrenheit, one per line. Add corresponding temperatures
together, convert the result to celsius, apply a 5-element moving average, and output to
celsius.txt.
Concatenation: Concatenate two files, fahrenheit1.txt, and fahrenheit2.txt, into a
single logical stream, apply the same transformation as above and output to celsius.txt
.
Dynamic resource allocation: Read a file, fahrenheits.txt, containing a list of
filenames. Concatenate these files into a single logical stream, convert this stream to
celsius, and output the joined stream to celsius.txt.
Multi-sink output: As above, but rather than producing a single output file, produce an
output file for each input file in fahrenheits.txt. Name the output file by appending
.celsius onto the input file name.
Internal effects: Given a stream of HTTP requests, parse each into some object and use it
to construct a database query. Execute this query, generating a stream of rows, which are
further processed using other stream transformations before being assembled into an
HTTP response. Here, the effect is no longer just a sink—we need to get back a result
and continue processing.
These scenarios can't be expressed with our existing API without dropping
down into normal, low-level IO monad programming (can you see why?).
Although we can try just adding more special cases to Source (perhaps a Zip
constructor, then an Append constructor, etc.), we can see this getting ugly,
especially if done naively.10 It seems we need a more principled way of extending
Process. This is what we will consider next.
www.it-ebooks.info
283
Footnote 10mStill, you may be interested to explore this approach. It is a challenging design exercise—the
problem is coming up with a nice, small set of primitive combinators that lets us express all the programs we wish
to write. We don't want to have 50 special cases which, in addition to being ugly, makes writing the collect
function extremely complicated. If you decide to experiment with this approach, think about what combinators
are needed to express each of these scenarios and any others you can think of. Can the combinator be expressed
using existing primitives in a resource safe way? If not, you can try adding another primitive case for it, refining
your primitives as we did throughout part 2, and updating your collect function to handle additional cases
in a resource-safe way.
trait Process[F[_],O]
object Process {
case class Await[F[_],A,O](
req: F[A], recv: A => Process[F,O],
finalizer: Process[F,O]) extends Process[F,O]
www.it-ebooks.info
284
We use the same smart constructors as before, emitAll and emit, with
similar definitions:
We will also introduce the helper function, await, which just curries the
Await constructor for better type inference:
www.it-ebooks.info
285
Let's see what else we can express with this new Process type. The F
parameter gives us a lot of flexibility.
15.4.1 Sources
Before, we were forced to introduce a separate type to represent sources. Now, we
can represent an effectful source using a Process[IO,O].11
Footnote 11mThere are some issues with making this representation resource-safe that we'll discuss shortly.
Whereas before, Source was a completely separate type from Process, now
it is merely a particular instance of it! To see how Process[IO,O] is indeed a
source of O values, consider what the Await constructor looks like when we
substitute IO for F:
Thus, any requests of the 'external' world can be satisfied, just by running the
IO action. If this action returns an A successfully, we invoke the recv function
with this result. If the action throws a special exception (perhaps called End) it
indicates normal termination and we switch to the fallback state. And if the
action throws any other exception, we switch to the cleanup state. Below is
simple interpreter of Source which collects up all the values emitted:
Here is the exception type End that we use for signaling normal termination.12
Footnote 12mThere are some design decisions here—we are using an exception, End, for control flow, but we
could choose to indicate normal termination with Option, say with type Step[A] =
IO[Option[A]], then having Process[Step,O] represent sources. We could also choose to pass the
exception along to the recv function, requiring the recv function to take an Either[Throwable,A].
We are adopting the convention that any exceptions that bubble all the way up to collect are by definition
unrecoverable. Programs can certainly choose to throw and catch exceptions internally if they wish.
www.it-ebooks.info
286
Normal termination
Helper function, defined below
trait Partial[F[_]] {
def attempt[A](a: F[A]): F[Either[Throwable,A]]
def fail[A](t: Throwable): F[A]
}
Rather than invoking run on our IO values, we can simply flatMap into the
req to obtain the result. We define a function on Process[F,O] to produce an
F[IndexedSeq[O]] given a Monad[F] and a Partial[F]:
www.it-ebooks.info
287
Unlike the simple tail recursive collect function above, this implementation
is no longer tail recursive, which means our Monad instance is now responsible for
ensuring constant stack usage. Luckily, the IO type we developed in chapter 13 is
already suitable for this, and as an added bonus, it supports the use of
asynchronous I/O primitives as well.
www.it-ebooks.info
288
cleanup argument to the Await, which again the collect function will
ensure gets called should errors occur.
As an example, let's use this policy to create a Process[IO,O] backed by
the lines of a file. We define it terms of the more general combinator, resource,
the Process analogue of the bracket function we introduced earlier for IO:
So far so good. However, we cannot only make sure that lines keeps its
fallback and cleanup parameters up to date whenever it produces an Await
—we need to make sure they actually get called. To see a potential problem,
consider collect(lines("names.txt") |> take(5)). The take(5)
process will halt early after only 5 elements have been received, possibly before
the file has been exhausted. It must therefore make sure before halting that
cleanup of lines is run. Note that collect cannot be responsible for this,
since collect has no idea that the Process it is interpreting is internally
composed of two other Process values, one of which requires finalization.
Thus, we have our second simple rule to follow: any process, d, which pulls
www.it-ebooks.info
289
values from another process, p, must ensure the cleanup action of p is run before
d halts.
This sounds rather error prone, but luckily, we get to deal with this concern in
just a single place, the |> combinator. We'll show how that works shortly in the
next section, when we show how to encode single-input processes using our
general Process type.
It is a bit strange to define the type f inside of One. Let's unpack what's going
on. Notice that f takes one parameters, X, but we have just one instance, Get,
which fixes X to be the I in the outer One[I]. Therefore, the type One[I]#f13
can only ever be a request for a value of type I! Moreover, we get evidence that X
is equal to I in the form of the Eq[X,I] which comes equipped with a pair of
functions to convert between the two types.14 We'll see how the Eq value gets used
a bit later during pattern matching. But now that we have all this, we can define
Process1 as just a type alias:
Footnote 13mNote on syntax: recall that if x is a type, x#foo references the type foo defined inside x.
Footnote 14mWe are prevented from instantiating an Eq[Int,String], say, because there is only one
public constructor, Eq.refl[A], which takes just a single type parameter and uses the identity function for
both to and from.
www.it-ebooks.info
290
To see what's going on, it helps to substitute the definition of Is[I]#f into a
call to Await:
From the definition of One[I]#f, we can see that req has just one possible
value, Get: f[I]. Therefore, recv must accept an I as its argument, which
means that Await can only be used to request I values. This is important to
understand—if this explanation didn't make sense, try working through these
definitions on paper, substituting the type definitions.
Our Process1 alias supports all the same operations as our old single-input
Process. Let's look at a couple. We first introduce a few helper functions to
improve type inference when calling the Process constructors:
def emit1[I,O](h: O,
tl: Process1[I,O] = halt1[I,O]): Process1[I,O] =
emit(h, tl)
Using these, our definitions of, for instance, lift and filter look almost
identical to before, except they return a Process1:
www.it-ebooks.info
291
Let's look at process composition next. The implementation looks very similar
to before, but we make sure to run the finalizer of the left process before the
right process halts. (Recall that we are using the finalizer argument of Await
to finalize resources—see the implementation of the resource combinator from
earlier.)
@annotation.tailrec
final def kill[O2]: Process[F,O2] = this match {
case Await(req,recv,fb,c) => c.drain
case Halt() => Halt()
case Emit(h, t) => t.kill
}
www.it-ebooks.info
292
Note that |> is defined for any Process[F,O] type, so this operation works
for transforming a Process1 value, an effectful Process[IO,O], and the
two-input Process type we will discuss next.
With |>, we can add convenience functions on Process for attaching various
Process1 transformations to the output. For instance, here's filter, defined
for any Process[F,O]:
We can add similar convenience functions for take, takeWhile, and so on.
See the chapter code for more examples.
This looks quite similar to our Is type from earlier, except that we now have
two possible values, L and R, and we get an Either[Eq[X,I], Eq[X,I2]]
for pattern matching. With T, we can now define a type alias, Tee, which accepts
www.it-ebooks.info
293
two inputs:
Once again, we define a few convenience functions for building these particular
types of Process:
def awaitL[I,I2,O](
recv: I => Tee[I,I2,O],
fallback: Tee[I,I2,O] = haltT[I,I2,O]): Tee[I,I2,O] =
await[T[I,I2]#f,I,O](L)(recv, fallback)
def awaitR[I,I2,O](
recv: I2 => Tee[I,I2,O],
fallback: Tee[I,I2,O] = haltT[I,I2,O]): Tee[I,I2,O] =
await[T[I,I2]#f,I2,O](R)(recv, fallback)
Let's define some Tee combinators. Zipping is a special case of Tee—we read
from the left, then the right (or vice versa), then emit the pair. Notice we get to be
explicit about the order we read from the inputs, a capability that can be important
when a Tee is talking to streams with external effects.16
Footnote 16mWe may also wish to be inexplicit about the order of the effects, allowing the driver to choose
nondeterministically and allowing for the possibility that the driver will execute both effects concurrently. See the
chapter notes and chapter code for some additional discussion of this.
This transducer will halt as soon as either input is exhausted, just like the zip
funtion on List. Let's define a zipWithAll which continues as long as either
input has elements. We accept a value to 'pad' each side with when its elements run
out:
www.it-ebooks.info
294
This uses a few helper functions—passR and passL ignore one of the inputs
to a Tee and echo the other branch.
awaitLOr and awaitROr just call await with the fallback argument as
the first argument, which is a bit more readable here.
There are a lot of other Tee combinators we could write. Nothing requires that
we read values from each input in lockstep. We could read from one input until
some condition is met, then switch to the other; we could read five values from the
left, then ten values from the right, read a value from the left then use it to
determine how many values to read from the right, and so on.
We will typically want to feed a Tee by connecting it to two processes. We can
define a function on Process that combines two processes using a Tee. This
function works for any Process type:
www.it-ebooks.info
295
This uses two helper functions, feedL and feedR, which serve the same
purpose as before—to feed the Tee in a loop as long as it expects values from
either side. See the chapter code for the full definition.
The one subtlety in this definition is we make sure to run cleanup for both
inputs before halting. What is nice about this overall approach is that we have
exactly four places in the library where we must do anything to ensure resource
safety: tee, |>, resource and the collect interpreter. All the other client
code that uses these and other combinators is guaranteed to be resource safe.
15.4.5 Sinks
How do we perform output using our Process type? We will often want to send
the output of a Source[O] to some Sink (perhaps sending a
Source[String] to an output file). Somewhat surprisingly, we can represent
sinks in terms of sources!
www.it-ebooks.info
296
That was easy. And notice what isn't included—there is no exception handling
code here—the combinators we are using guarantee that the FileWriter will be
closed if exceptions occur or when whatever is feeding the Sink signals it is done.
We can use tee to implement a combinator to, which pipes the output of a
Process to a Sink:
When run via collect, this will open the input file and the output file and
incrementally transform the input stream, ignoring commented lines.
www.it-ebooks.info
297
Channel is useful when a pure pipeline must execute some I/O action as one
of its stages. A typical example might be an application that needs to execute
database queries. It would be nice if our database queries could return a
Source[Row], where Row is some representation of a database row. This would
allow the program to process the result set of a query using all the fancy stream
transducers we've built up so far.
Here's a very simple query executor, which uses Map[String,Any] as the
(untyped) row representation:
www.it-ebooks.info
298
www.it-ebooks.info
299
map(_ toString).
to(fileW(file + ".celsius"))
} yield ()) drain
There are additional examples using this library in the chapter code.
15.5 Applications
The ideas presented in this chapter are extremely widely applicable. A surprising
number of programs can be cast in terms of stream processing—once you are
aware of the abstraction, you begin seeing it everywhere. Let's look at some
domains where it is applicable:
File I/O: We've already demonstrated how to use stream processing for file I/O. Although
we have focused on line-by-line reading and writing for the examples here, we can also
use the library for processing binary files.
Message processing, state machines, and actors: Large systems are often organized as a
system of loosely-coupled components that communicate via message passing. These
systems are often expressed in terms of actors, which communicate via explicit message
sends and receives. We can express components in these architectures as stream
processors, which lets us describe extremely complex state machines and behaviors while
retaining a high-level, compositional API.
Servers, web applications: A web application can be thought of as converting a stream of
HTTP requests to a stream HTTP responses.
UI programming: We can view individual UI events such as mouseclicks as streams, and
the UI as one large network of stream processors determining how the UI responds to
user interaction.
Big data, distributed systems: Stream processing libraries can be distributed and
parallelized for processing large amounts of data. The key insight here is that Process
values being composed need not all live on the same machine.
If you're curious to learn more about these applications (and others), see the
chapter notes for additional discussion and links to further reading. The chapter
www.it-ebooks.info
300
notes and code also discuss some extensions to the Process type we discussed
here, including the introduction of nondeterministic choice which allows for
concurrent evaluation in the execution of a Process.
15.6 Conclusion
We began this book with the introduction of a simple premise: that we assemble
our programs using only pure functions. From this sole premise and its
consequences we were led to develop a new approach to programming, one with its
own ideas, techniques, and abstractions. In this final chapter, we constructed a
library for stream processing and incremental I/O, demonstrating that we can retain
the compositional style developed throughout this book even for programs that
interact with the outside world. Our story is now complete of how to use FP to
architect programs both large and small.
While good design is always hard, over time, expressing code functionally
becomes effortless. By this point, you have all the tools needed to start functional
programming, no matter the programming task. FP is a deep subject, and as you
apply it to more problems, new ideas and techniques will emerge. Enjoy the
journey, keep learning, and good luck!
www.it-ebooks.info
301
Index Terms
causal streams
compositionality
compositional style
composition of processes
driver
early termination
equality witness
fusion
imperative programming
incremental I/O
lazy I/O
memoization
multi-input transducer
pipe
pipeline
resource
resource leak
resource safety
resource safety
resource safety
resource safety
smart constructor
sources
state machine
stream processor
stream transducer
Tee
Wye
www.it-ebooks.info