“The owls are not what they seem.”
Goals
The goal of this lesson is to review at a high level how for
loops work in Scala. This is necessary because Scala/FP developers take advantage of advanced Scala for
loop features.
As an example of what I mean, the goal of the next few lessons is to explain what’s happening in this for
loop:
def stackManip: State[Stack, Int] = for {
_ <- push(3)
a <- pop
b <- pop
} yield(b)
If you already understand what’s happening in that code, feel free to skip over these lessons; otherwise, read on.
Introduction
If you’re used to using the for
loop in Java, Scala’s for
loop can work similar to it, but it can also be used in a different manner.
A very different manner.
And Scala/FP developers use it in this other manner.
A lot.
For instance, some experienced Scala/FP developers say that this is a “simple” use of a for
loop in a functional style:
def stackManip: State[Stack, Int] = for {
_ <- push(3)
a <- pop
b <- pop
} yield(b)
If you’re used to traditional for
loops in languages like C and Java, whatever is happening in this “loop” can be quite a surprise.
A peek into what’s happening
I can explain a little of what’s happening: This code is working with a stack, and a push
function is pushing the number 3
onto a pre-existing stack, and then the next two lines are popping values off of the stack. But even knowing that, a few questions come to mind:
- If a
push
function pushes a number onto a stack, where exactly is that stack? - A
push
function normally doesn’t return anything meaningful, so how is it being used as a generator in afor
loop? And what is that underscore character doing on the left side of thepush
expression? - Assuming that those
pop
functions are popping data off of a stack, again I ask, where is that stack?
In this lesson we’ll starting digging into how the Scala for
comprehension works. Knowing how it works is a prerequisite for understanding how this stackManip
method works, and how for
is used in many Scala/FP situations.
New rule: No more “for
loop”
Hmmm ... if you go back and look at that code again, is it really a “loop”? No, not really. However that code works, all it seems to do is to perform three operations on a stack. This thought leads us to a new rule:
For a variety of reasons — including the fact that this code isn’t really a loop but something called a comprehension — I will no longer use the term “for
loop.” From now on, I’ll refer to this construct as a “for
comprehension.”
I can explain this name by once again taking a short look back at history and mathematics.
for
comprehension history
As a quick historical tidbit, the name “comprehension” comes from both Haskell and mathematics, with mathematics preceding Haskell by some large number of years. The book, Haskell, the Craft of Functional Programming, provides this introduction:
“One of the features of a functional language is the list comprehension notation, which has no parallels in other paradigms.”
LYAHFGG states:
“List comprehensions are a way to filter, transform, and combine lists. They’re very similar to the mathematical concept of set comprehensions. Set comprehensions are normally used for building sets out of other sets.”
If you’ve used the Scala for/yield expression at all, you know that it meets this definition. Indeed, you’ll find the for
construct described on a page on the official Scala website titled Sequence Comprehensions. There they state:
Comprehensions have the form
for (enumerators) yield e
, where enumerators refers to a semicolon-separated list of enumerators. An enumerator is either a generator which introduces new variables, or it is a filter.
These definitions lead us to the for
comprehension concepts of generators, filters, and definitions.
Generators, filters, and definitions
A Scala for
comprehension can contain the following three expressions:
- Generators
- Filters
- Definitions
These are shown in the following source code snippet from the book, Programming in Scala:
for {
p <- persons // generator
n = p.name // definition
if (n startsWith "To") // filter
} yield
Let’s look at short, formal definitions for each of these elements.
Generators
Generators have this general form:
pattern <- expression
In the simple case this results in an assignment, like this:
p <- persons
In this expression the value p
iterates over all of the elements contained in persons
. Generators can be more complicated than this, but this is their most simple and common use.
There are three more things to know about generators:
- Every
for
comprehension begins with a generator for
comprehensions can have multiple generators-
The left side of a generator can also be a pattern:
def getTheSquirrel = for { (dog, cat, squirrel) <- getAnimalsOutOfLaundryRoom } yield squirrel
Definitions
for
comprehension definitions have this general form:
pattern = expression
A definition binds the pattern pattern
on the left to the value of expression
on the right.
In the original example in this lesson:
n = p.name
the variable n
is bound to the value p.name
. That statement has the same effect as writing this code outside of a for
comprehension:
val n = p.name
Filters
for
comprehension filters have this general form:
if (expression)
In this code expression
must have the type Boolean
.
Filters drop all elements from the iteration for which expression
returns false
, so in this code:
if (n startsWith "To")
any value n
that does not start with the string "To"
will be dropped during the iteration process.
Stated the other way, any value
n
that begins with the string"To"
will be retained during the iteration process.
An example
Here’s a simple example of each of these features in a Scala for
comprehension:
case class Person(firstName: String, lastName: String)
val people = List(
Person("barney", "rubble"),
Person("fred", "flintstone")
)
val namesStartingWithB = for {
p <- people // generator
fname = p.firstName // definition
if (fname startsWith "b") // filter
} yield fname.toUpperCase
If you put that code in the Scala REPL, you can then run this code to see the expected result:
scala> namesStartingWithB.foreach(println)
BARNEY
Summary
What you started to see in this lesson is that Scala for
loops are much more than “loops.” They are based on mathematical set comprehensions, and they are a way to “filter, transform, and combine lists.”
You also saw that a Scala for
comprehension can contain the following three types of expressions:
- Generators
- Filters
- Definitions
What’s next
This lesson provided a quick review of the basic features of for
comprehensions as background for the next lesson, where you’ll learn how to write your own custom “collection” class to work in a for
comprehension.
As a reminder, the end goal of these lessons is to help you understand how for
comprehensions like this work:
def stackManip: State[Stack, Int] = for {
_ <- push(3)
a <- pop
b <- pop
} yield(b)