Copyright © Cay S. Horstmann 2015
This work is licensed under a Creative Commons Attribution 4.0 International License
We can extend the usage of higher order functions on sequences to many
calculations which are usually expressed using nested loops.
Example: Given a positive integer n
, find all pairs of positive integers i
and j
, with 1 <= j < i < n
such that i + j
is prime.
For example, if n = 7
, the sought pairs are
A natural way to do this is to:
(i, j)
such that 1 <= j < i < n
.i + j
is prime.One natural way to generate the sequence of pairs is to:
i
between 1
and n
(excluded).i
, generate the list of pairs (i, 1), ..., (i, i-1)
.This can be achieved by combining until
and map
:
(1 until n) map (i =>
(1 until i) map (j => (i, j)))
What is the type of the result?
(1 until n) map (i =>
(1 until i) map (j => (i, j)))
The previous step gave a sequence of sequences, let's call it xss
.
We can combine all the sub-sequences using foldRight
with ++
:
(xss foldRight Seq[Int]())(_ ++ _)
Or, equivalently, we use the built-in method flatten
xss.flatten
This gives:
((1 until n) map (i =>
(1 until i) map (j => (i, j)))).flatten
Here's a useful law:
xs flatMap f = (xs map f).flatten
Hence, the above expression can be simplified to
(1 until n) flatMap (i =>
(1 until i) map (j => (i, j)))
By reassembling the pieces, we obtain the following expression:
(1 until n) flatMap (i =>
(1 until i) map (j => (i, j))) filter ( pair =>
isPrime(pair._1 + pair._2))
This works, but makes most people's head hurt.
Is there a simpler way?
Higher-order functions such as map
, flatMap
or filter
provide powerful constructs for manipulating lists.
But sometimes the level of abstraction required by these function make the program difficult to understand.
In this case, Scala's for
expression notation can help.
Let persons
be a list of elements of class Person
, with fields name
and age
.
case class Person(name: String, age: Int)
To obtain the names of persons over 20 years old, you can write:
for ( p <- persons if p.age > 20 ) yield p.name
which is equivalent to:
persons filter (p => p.age > 20) map (p => p.name)
The for-expression is similar to loops in imperative languages, except
that it builds a list of the results of all iterations.
A for-expression is of the form
for ( s ) yield e
where s
is a sequence of generators and filters,
and e
is an expression whose value is returned by an iteration.
p <- e
, p
is a pattern and e
an expression whose value is a collection. if f
where f
is a boolean expression.Instead of ( s )
, braces { s }
can also be used, and then the
sequence of generators and filters can be written on multiple lines
without requiring semicolons.
Here are two examples which were previously solved with higher-order functions:
Given a positive integer n
, find all the pairs of positive integers
(i, j)
such that 1 <= j < i < n
, and i + j
is prime.
for {
i <- 1 until n
j <- 1 until i
if isPrime(i + j)
} yield (i, j)
Write a version of scalarProduct
(see last session) that makes use of
a for
:
def scalarProduct(xs: List[Double], ys: List[Double]) : Double =
In the following we are going to see Sets
which are another fundamental collection type.
We then combine Sets
and For Expressions
in a classical combinatorial search problem, namely the N-Queens problem.
Sets are another basic abstraction in the Scala collections.
So far, all the collections we have seen were sequences. But there are also two other two other fundamental class of collections: Sets
and Maps
A set is written analogously to a sequence:
val fruit = Set("apple", "banana", "pear")
//> fruit : scala.collection.immutable.Set[String] = Set(apple, banana, pear)
val s = (1 to 6).toSet
//> s : scala.collection.immutable.Set[Int] = Set(5, 1, 6, 2, 3, 4)
Most operations on sequences are also available on sets:
s map (_ + 2)
//> res1: scala.collection.immutable.Set[Int] = Set(5, 6, 7, 3, 8, 4)
fruit filter (_.startsWith("app"))
//> res2: scala.collection.immutable.Set[String] = Set(apple)
s.nonEmpty
//> res3: Boolean = true
(see Iterable
s Scaladoc for a list of all supported operations)
The principal differences between sets and sequences are:
Sets are unordered; the elements of a set do not have a predefined order in which they appear in the set
sets do not have duplicate elements:
s map (_ / 2) // Set(2, 0, 3, 1)
The fundamental operation on sets is contains
:
s contains 5 // true
The eight queens problem is to place eight queens on a chessboard so that no queen is threatened by another.
In other words, there can’t be two queens in the same row, column, or diagonal.
We now develop a solution for a chessboard of any size, not just 8.
One way to solve the problem is to place a queen on each row.
Once we have placed k - 1
queens, one must place the k
th queen in a column where it’s not “in check” with any other queen on the board.
We can solve this problem with a recursive algorithm:
k-1
queens on a board of size n
.k-1
) containing the numbers of columns (between 0
and n-1
).k-1
th row comes first in the list, followed by the column number of the queen in row k-2
, etc. k
th queen, we generate all possible extensions of each solution preceded by a new queen: def queens(n: Int) = {
def placeQueens(k: Int): Set[List[Int]] = {
if (k == 0) Set(List())
else
for {
queens <- placeQueens(k - 1)
col <- 0 until n
if isSafe(col, queens)
} yield col :: queens
}
placeQueens(n)
}
Write a function
def isSafe(col: Int, queens: List[Int]): Boolean
which tests if a queen placed in an indicated column col
is secure amongst the other placed queens.
It is assumed that the new queen is placed in the next availabale row after the other placed queens (in other words: in row queens.length
).
Steps:
queens
list, the new queen given by col
is not in check:for
The for notation is essentially equivalent to the common operations of
query languages for databases.
Example: Suppose that we have a database books
, represented as a list of books.
case class Book(title: String, authors: List[String])
val books: List[Book] = List(
Book(title = "Structure and Interpretation of Computer Programs",
authors = List("Abelson, Harald", "Sussman, Gerald J.")),
Book(title = "Introduction to Functional Programming",
authors = List("Bird, Richard", "Wadler, Phil")),
Book(title = "Effective Java",
authors = List("Bloch, Joshua")),
Book(title = "Java Puzzlers",
authors = List("Bloch, Joshua", "Gafter, Neal")),
Book(title = "Programming in Scala",
authors = List("Odersky, Martin", "Spoon, Lex", "Venners, Bill")))
To find the titles of books whose author’s name is “Bird”:
for (b <- books; a <- b.authors if a startsWith "Bird,")
yield b.title
To find all the books which have the word “Program” in the title:
for (b <- books if b.title indexOf "Program" >= 0)
yield b.title
To find the names of all authors who have written at least two books present in the database.
for {
b1 <- books
b2 <- books
if b1 != b2
a1 <- b1.authors
a2 <- b2.authors
if a1 == a2
} yield a1
Why do solutions show up twice?
How can we avoid this?
To find the names of all authors who have written at least two books present in the database.
for {
b1 <- books
b2 <- books
if b1.title < b2.title
a1 <- b1.authors
a2 <- b2.authors
if a1 == a2
} yield a1
What happens if an author has published three books?
Another possibility is to use the operation withDefaultValue
that turns a map into a
total function:
val cap1 = capitalOfCountry withDefaultValue "<unknown>"
cap1("Andorra") // "<unknown>"
You can take look at different operations in class Map here.
Two useful operation of SQL queries in addition to for-expressions are
groupBy
and orderBy
.
orderBy
on a collection can be expressed by sortWith
and sorted
.
val fruit = List("apple", "pear", "orange", "pineapple")
fruit sortWith (_.length < _.length) // List("pear", "apple", "orange", "pineapple")
fruit.sorted // List("apple", "orange", "pear", "pineapple")
groupBy
is available on Scala collections. It partitions a collection
into a map of collections according to a discriminator function f
.
Example:
fruit groupBy (_.head) //> Map(p -> List(pear, pineapple),
//| a -> List(apple),
//| o -> List(orange))
A polynomial can be seen as a map from exponents to coefficients.
For instance, can be represented with the map.
Map(0 -> 5, 1 -> -2, 3 -> 1)
Based on this observation, let’s design a class Polynom
that represents polynomials as maps.
class Poly(val terms0: Map[Int, Double]) {
val terms = terms0 withDefaultValue 0.0
def + (other: Poly) : Poly = {
val newKeySet = terms.keySet ++ other.terms.keySet
val setOfTuples = for(x <- newKeySet) yield
( x -> (terms(x) + other.terms(x)))
new Poly(setOfTuples.toMap)
}
}
It’s quite inconvenient to have to write
Polynom(Map(1 -> 2.0, 3 -> 4.0, 5 -> 6.2))
Can one do without the Map(...)
?
Problem: The number of key -> value
pairs passed to Map
can vary.
We can accommodate this pattern using a repeated parameter:
def Polynom(bindings: (Int, Double)*) =
new Polynom(bindings.toMap withDefaultValue 0)
Polynom(1 -> 2.0, 3 -> 4.0, 5 -> 6.2)
Inside the Polynom
function, bindings
is seen as a
Seq[(Int, Double)]
.
class Poly(val terms0: Map[Int, Double]) {
def this(bindings: (Int, Double)*) = this(bindings.toMap)
val terms = terms0 withDefaultValue 0.0
def + (other: Poly) : Poly = {
val newKeySet = terms.keySet ++ other.terms.keySet
val setOfTuples = for(x <- newKeySet) yield
( x -> (terms(x) + other.terms(x)))
new Poly(setOfTuples.toMap)
}
override def toString =
(for ((exp, coeff) <- terms.toList.sorted.reverse)
yield coeff+"x^"+exp) mkString " + "
}
Using for expressions write a function to find the numbers x,y,z below n such that
Do this as individual work, not with your partner
When all done, email the signed zip files to Fatemeh.Borran@heig-vd.ch