Functional Programming Principles in Scala | Coursera
Week 1
Main programming paradigms:
- imperative programming ⇒ instruction sequences for a Von Neumann computer (processor, memory, bus)
- Von Neumann bottleneck ⇒ conceptualizing data strictures word-by-word
- So, we need a higher level idea of programming in order to scale up programs
- Von Neumann bottleneck ⇒ conceptualizing data strictures word-by-word
- functional programming
- logic programming
Object-oriented programming is orthogonal to these paradigms because can be used with imperative programming or with functional programming.
We want to implement mathematical theories as functions in code, so we can reason using those theories at a high level. Most theories do not support mutation, so we need to abandon mutation.
Two ways to look at functional programming:
- restricted sense: no mutable variables, assignments, or imperative control structures (e.g. for loops)
- wider sense: programs which focus on functions
lambda calculus ⇒ substitution model for reducing expressions to simpler forms, works as long as expressions:
- don’t have side effects (e.g.
a += 1
has a side effect of storinga
) - terminate (e.g.
def loop = loop
does not terminate)
Two equivalent evaluation strategies for lambda calculus:
-
call by value ⇒ evaluate function arguments before function
def square(x: Double): Double = x * x /** An example evaluation: 1. square(1 + 2) 2. square(3) 3. 3 * 3 4. 9 **/
- only evaluates every function argument 1 time
- works better with imperative programming paradigm since we fix the time at which we evaluate function arguments
-
call by name ⇒ evaluate function first, then arguments if need be
def square(x: => Double): Double = x * x /** An example evaluation: 1. square(1 + 2) 2. (1 + 2) * (1 + 2) 3. 3 * (1 + 2) 4. 3 * 3 5. 9 **/
- function argument will not be evaluated if it is unused in the function body
- So, call by value termination ensures call by name termination
- function argument will not be evaluated if it is unused in the function body
Two equivalent definition strategies:
- definition by value ⇒ righthand side is evaluated at the point of definition e.g.
val x = square(3)
- definition by name ⇒ righthand side is evaluated on each use e.g.
def three: Int = 3
Example: Computing square roots with Newton’s method
Suppose we wish to find the square root of 2. According to Newton’s method, we guess 1. Next, we find the average of 2 divided by our guess (2/1 = 2) and our guess (1), which is 1.5. So the next guess will be 1.5.
import scala.math.abs
def sqrt(x: Double): Double = {
// Nest helper functions in block to reduce namespace pollution
// We don't need to pass constant x to any of these helper functions!
def sqrtIter(guess: Double): Double =
if (isGoodEnough(guess)) guess else sqrtIter(improve(guess))
// Divide by x in order to scale for very small/large values for guess
def isGoodEnough(guess: Double) =
abs((guess * guess) - x) / x < 0.001
def improve(guess: Double) =
(x / guess + guess) / 2
// Always guess 1.0
sqrtIter(1.0, x)
}
tail recursion ⇒ a function doing a tail call (calling itself or another function as its last action) can reuse its stack frame and thus iterate in constant space
// Tail recursive
def gcd(a: Int, b: Int): Int =
if (b == 0) a else gcd(b, a % b)
// NOT tail recursive, since we have to multiply by n after calling factorial
def factorial(n: Int): Int =
if (n == 0) 1 else n * factorial(n - 1)
// Tail recursive
def factorialTR(n: Int): Int = {
def loop(acc: Int, n: Int): Int
if (n == 0) acc else loop(acc * n, n - 1)
loop(1, n)
}
Week 2
higher order function ⇒ function that takes other functions as parameters or returns a function as results
// A higher order function
// f: Int => Int means a function f with parameter Int that returns Int
def sum(f: Int => Int, a: Int, b: Int): Int = {
def loop(a: Int, acc: Int): Int = {
if (a > b) acc
else loop(a + 1, acc + f(a))
}
loop(a, 0)
}
def id(x: Int): Int = x
def cube(x: Int): Int = x * x * x
def factorial(n: Int): Int = if (n == 0) 1 else n * factorial(n - 1)
// Or, use anonymous function syntactic sugar; x => x
def sumInts(a: Int, b:Int) = sum(id, a, b)
// Or, use anonymous function syntactic sugar; x => x * x * x
def sumCubes(a: Int, b: Int) = sum(cube, a, b)
def sumFactorials(a: Int, b: Int) = sum(factorial, a, b)
Notice that in sumInts
, sumCubes
, and sumFactorials
, we accept parameters a
and b
and we simply pass them on. We can avoid this repetition like so:
// Syntactic sugar for function returning functions
// Its type is (Int => Int) => ((Int, Int) => Int)
def sum(f: Int => Int)(a: Int, b: Int): Int =
if (a > b) 0 else f(a) + sum(f)(a + 1, b)
// Then, we can define w/o (a, b) and use these like sumCubes(1, 10) as before
def sumInts = sum(x => x)
def sumCubes = sum(x => x * x * x)
def sumFactorials = sum(factorial)
// Or, use the general function directly
sum(cube)(1, 10)
In general, a definition of a function with n parameter lists applied in sequence, where n > 1
\[\text{def f}(args_1)\cdots(args_n) = E\]can be rewritten as:
\[\text{def f}(args_1)\cdots(args_{n - 1}) = \{ \text{def g}(args_n) = E; g\}\]or for short, without g:
\[\text{def f}(args_1)\cdots(args_{n - 1}) = (args_n \rightarrow E)\]Rewriting n times, we have:
\[\text{def f} = (args_1 \rightarrow (args_2 \rightarrow \cdots (args_n \rightarrow E)\cdots))\]Defining functions in this style is called currying.
Now let’s write a function product
that computes the products from integers in the range a
to b
using currying, like we did before with sum
.
def product(f: Int => Int)(a: Int, b: Int): Int =
if (a > b) 1 else f(a) * product(f)(a + 1, b)
def factorial(n: Int) = product(x => x)(1, n)
And, a more general function that generalizes both sum
and product
:
// f => map function to apply to each element
// combine => reduce elements after mapping
// zero => value to return if the range is empty
def mapReduce(f: Int => Int, combine: (Int, Int) => Int, zero: Int)(a: Int, b: Int): Int =
if (a > b) zero else combine(f(a), mapReduce(f, combine, zero)(a + 1, b))
def product = mapReduce(f, (x, y) => x * y, 1)(a, b)
def sum = mapReduce(f, (x, y) => x + y, 0)(a, b)
Example: Finding a fixed point of a function
A number x is a fixed point of a function f if f(x) = x. To find the fixed point x of some function f, we can start with an initial estimate for x, and compute f(x). If f(x) is sufficiently close to x, we’ve found x. If not, we try f(f(x)) as our new estimate. We can repeat this process until we get a sufficiently close x. (This only works for some functions.)
val tolerance = 0.0001
def isCloseEnough(x: Double, y: Double) =
abs((x - y) / x) / x < tolerance
def fixedPoint(f: Double => Double)(firstGuess: Double) = {
def iterate(guess: Double): Double = {
val next = f(guess)
if (isCloseEnough(guess, next)) next
else iterate(next)
}
iterate(firstGuess)
}
// Find the fixed point of f(x) = 1 + x / 2
fixedPoint(x => 1 + x/2)(1)
Recall that the square root of x is defined as y such that y * y = x. Dividing by y on both sides, we have that the square root of x is y such that y = x / y. So, the square root of x is the fixed point y of the function y ⇒ x / y.
def sqrt(x: Double) = fixedPoint(y => x / y)(1)
// Unfortunately, this doesn't converge (guesses oscillate between 1 and 2)
sqrt(2)
// To fix this, we can average our last guess (y) and the current guess (x/y)
def sqrt(x: Double) = fixedPoint(y => (y + x / y) / 2)(1)
// Stabilizing by averaging can be written in general as:
def averageDamp(f: Double => Double)(x: Double) = (x + f(x)) / 2
// Using this in sqrt, we have
def sqrt(x: Double) = fixedPoint(averageDamp(y => x / y))(1)
Let’s write a class for rational numbers in Scala.
// Implicit primary constructor
class Rational(x: Int, y: Int) {
require(y != 0, "denominator must be nonzero")
// Alternate constructor
def this(x: Int) = this(x, 1)
private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)
// We could also simplify only at print time, but we could run into overflow
def numer = x / gcd(x, y)
def denom = y / gcd(x, y)
def < (that: Rational) = numer * that.denom < that.numer * denom
def max(that: Rational) = if (this < that) that else this
def + (that: Rational) =
new Rational(
numer * that.denom + that.numer * denom,
denom * that.denom
)
def unary_- : Rational = new Rational(-numer, denom)
def - (that: Rational) = this + -that
override def toString = numer + "/" + denom
}
val x = new Rational(1, 2)
x.numer // 1
x.denom // 2
val y = new Rational(2, 3)
x + y // 7/6
val strange = new Rational(1, 0) // IllegalArgumentException from require
require ⇒ used to enforce a precondition on the caller of a function
assert ⇒ used to check the code of the function itself
We can formally define the evaluation of a class method call using substitution much the same way as we defined the evaluation of a function call.
new Rational(1, 2).numer
// [1/x, 2/y][][new Rational(1, 2) / this] x
// --------- substitute class parameters x and y
// -- substitute function parameters for numer
// --------------------------- sustitute this for Rational object
// - function body
// Evaluation:
// = 1
new rational(1, 2).less(new Rational(2, 3))
// [1/x, 2/y][new Rational(2, 3) / that][new Rational(1, 2) / this]
// this.numer * that.denom < that.numer * this.denom
// Evaluation:
// = new Rational(1, 2).numer * new Rational(2, 3).denom <
// new Rational(2, 3).numer * new Rational(1, 2).denom
// = 1 * 3 < 2 * 2
// = true
Any method with a parameter can be used like an infix operator:
r max s === r.max(s)
Identifiers in Scala can be:
- alphanumeric (includes
_
) - symbolic
- a mix of alphanumeric and symbolic
Precedence of an operator is determined by its first character. In order of increasing priority:
- All letters
-
- &
- < >
- = !
-
- -
-
- / %
- All other special characters
// Evaluation of precendence example:
(a + b ^? c ?^ d less a ==> b | c) === (
((a + b) ^? (c ?^ d)) less ((a ==> b) | c)
)
Week 3
Let’s write a class for sets of integers to demonstrate class inheritance.
abstract class IntSet {
def contains(x: Int): Boolean
def incl(x: Int): IntSet
def union(other: IntSet): IntSet
}
// Let's implement IntSet as a binary tree
// object keyword => singleton value, since all instances of Empty are the same
object Empty extends IntSet {
def contains(x: Int): Boolean = false
def incl(x: Int): Intset = new NonEmpty(x, Empty, Empty) // not `new Empty`
def union(other: IntSet): IntSet = other
override def toString = "."
}
// NonEmpty has the base classes IntSet and Object
// Object is a default base class for all user-defined classes, like in Java
class NonEmpty(elem: Int, left: IntSet, right: IntSet) extends IntSet {
def contains(x: Int): Boolean =
if (x < elem) left contains x
else if (x > elem) right contains x
else true
def incl(x: Int): IntSet =
if (x < elem) new NonEmpty(elem, left incl x, right)
else if (x > elem) new NonEmpty(elem, left, right incl x)
else this
// this terimnates because each call to union is on a set that is smaller
def union(other: IntSet): IntSet = ((left union right) union other) incl elem
// You cannot override a base class method without the override keyword
// Using override without a base class method being overriden is legal, but
// gives a warning
override def toString = "{" + left + elem + right + "}"
}
dynamic method dispatch ⇒ code invoked by a method call depends on the runtime type of the object that contains the method
Notice that dynamic method dispatch is analogous to calls to higher order functions.
Classes and objects are organized in packages, like in Java.
package profun.examples
import week3.IntSet // import IntSet from week 3
import week2.{Rational, FunSet} // import 2 things from week 2
import week1._ // import everything from week 1
// places Hello in the pacakge "profun.examples"
object Hello {
// run in cmdline with `scala profun.examples.Hello`
def main(args: Array[String]) = println("hello world!")
}
Entities always imported in any Scala program:
- all members of package
scala
- all members of package
java.lang
- all members of the singleton object
scala.Predef
Scala is a single inheritance language, meaning that a class can only have one superclass. Use traits to allow a class to include code from several supertypes.
// Traits cannot have value parameters, unlike classes
trait Planar {
// Can contain fields and concrete methods
def height: Int
def width: Int
def surface = height * width
}
class Square extends Shape with Planar with Movable ...
We call two entities that can convert to each other a view of each other. A view relationship is not the same as a subtype/supertype relationship because:
- Converting to an alternate view requires a change in the representation of the data
- Converting to an alternate view may be lossy
Nothing
type in Scala is used for:
- element type of empty collections e.g.
Set[Nothing]
- signaling abnormal termination e.g.
throw Exception
has typeNothing
if (true) 1 else false // expression has type AnyVal
Let’s look at the cons list data structure. The cons list is an immutable list that consists of:
- Nil ⇒ the empty list, and
- Cons ⇒ cell containing the first element of the list and a pointer reference to the rest of the list
For example:
List(1, 2, 3)
/**
is represented as:
[]
| \
1 []
| \
2 []
| \
3 Nil
**/
List(List(true, false), List(3))
/**
is represented as:
[]
/ \_________
[] []
| \ | \
1 [] [] Nil
| \ | \
0 Nil 3 Nil
**/
Now let’s implement it.
trait List[T] {
def isEmpty: Boolean
def head: T
def tail: List[T]
}
// use of "val" in the constructor defines fields `head` and `tail`
// this also implements `def head` and `def tail` in the trait List[T]
class Cons[T](val head: T, val tail: List[T]) extends List[T] {
def isEmpty = false
}
class Nil extends List[T] {
def isEmpty = true
// Nothing is a subtype of T, so this definition works
def head: Nothing = throw new NoSuchElementException("Nil.head")
def tail: Nothing = throw new NoSuchElementException("Nil.tail")
}
def singleton[T](elem: T) = new Cons[T](elem, new Nil[T])
// use as:
singleton[Int](1)
singleton(true) // Scala compiler infers T as Boolean
// 0 index => 1st element
def nth[T](n: Int, xs: List[T]): T =
if(xs.isEmpty) throw new IndexOutOfBoundsException
else if(n == 0) xs.head
else nth(n - 1, xs.tail)
val list = new Cons(1, new Cons(2, new Cons(3, new Nil)))
nth(2, list) // 3
nth(-1, list) // IndexOutOfBoundsException
type erasure ⇒ type parameters are removed so that they cannot affect evaluation of the program
Polymorphism comes in two principal forms:
- subtyping ⇒ instances of a subclass can be referenced as a base class e.g.
class Square extends Shape
- generics ⇒ instances of a function/class can be created by type parameterization e.g.
List[T]
Week 4
pure object-oriented language ⇒ every value is an object, every operation is a method on some object
package idealized.scala
abstract class Boolean {
// if (cond) t else e === cond.ifThenElse(t, e)
def ifThenElse[T](t: => T, e: => T): T
// cond && x === if (cond) then x else false
def &&(x: => Boolean): Boolean = ifThenElse(x, false)
def ||(x: => Boolean): Boolean = ifThenElse(true, x)
def unary_!: Boolean = ifThenElse(false, true)
def ==(x: Boolean): Boolean = ifThenElse(x, x.unary_!)
def !=(x: Boolean): Boolean = ifThenElse(x.unary_!, x)
// Assume false < true
def <(x: Boolean): Boolean = ifThenElse(false, x)
}
object true extends Boolean {
// if (true) t else e === t
def ifThenElse[T](t: => T, e: => T): t
}
object false extends Boolean {
// if (false) t else e === e
def ifThenElse[T](t: => T, e: => T): e
}
A partial specification of class scala.Int
class Int {
// method overloading is allowed, as in Java
def + (that: Double): Double
def + (that: Float): Float
def + (that: Long): Long
def + (that: Int): Int // same for -, *, /, %
def << (cnt: Int): Int // same for >>
def & (that: Long): Long
def & (that: Int): Int // same for |, ^
def == (that: Double): Boolean
def == (that: Float): Boolean
def == (that: Long): Boolean // same for !=, <, >, <=, >=
}
// Natural number class (simplified Int class)
abstract class Nat {
def isZero: Boolean
def predecessor: Nat // returns the previous natural number before this
def successor: Nat = new Succ(this) // returns the next natural number after this
def + (that: Nat): Nat
def - (that: Nat): Nat
}
object Zero extends Nat {
def isZero: Boolean = true
def predecessor: Nat = throw new Error("0.predecessor")
def + (that: Nat): Nat = that
def - (that: Nat): Nat = if (that.isZero) this else throw new Error("negative number")
}
class Succ(n: Nat) extends Nat {
def isZero: Boolean = false
def predecessor: Nat = n
def + (that: Nat): Nat = new Succ(n + that)
// 5 - 2 => 4 - 1 => 3 - 0 => 3
def - (that: Nat): Nat = if (that.isZero) this else n - that.predecessor
}
We can also treat functions as objects.
package scala
// Function2, Function3 ... Function22 also exist to take more parameters
trait Function1[A, B] {
def apply(x: A): B
}
// We can write `def f(x: Int) => x * x` by eta-expansion as:
val f1 = {
class AnonFun extends Function1[Int, Int] {
def apply(x: Int) = x * x
}
new AnonFun
}
// or, using anonymous class syntax (also in Java):
val f2 = new Function1[Int, Int] {
// Notice, the apply method itself is not a function object, because that
// would be an infinite recursive definition
def apply(x: Int) = x * x
}
// Then, f(7) would be
f2.apply(7)
Let’s do an exercise with Lists to allow them to accept multiple parameters in the same way Function
does.
trait List[T] {
def isEmpty: Boolean
def head: T
def tail: List[T]
}
class Cons[T](val head: T, val tail: List[T]) extends List[T] {
def isEmpty = false
}
class Nil extends List[T] {
def isEmpty = true
def head: Nothing = throw new NoSuchElementException("Nil.head")
def tail: Nothing = throw new NoSuchElementException("Nil.tail")
}
object List {
def apply[T](x1: T, x2: T): List[T] = new Cons(x1, new Cons(x2, new Nil))
def apply[T](x: T): List[T] = new Cons(x, new Nil)
def apply[T]() = new Nil
}
// This lets us instantiate lists up to length 2
List() // the empty list
List(1) // list with single element 1
List(2, 3) // list with elements 2, 3
Type Bounds
Suppose we had a method assertAllPos
that for a given IntSet
, returns the IntSet
if all of its elements are positive and throws an exception otherwise. We could declare assertAllPos
as:
def assertAllPos(s: IntSet): IntSet
But return type IntSet
doesn’t capture the fact that assertAllPos
can return Empty
, NonEmpty
, or throw an error. Instead, we could use bounds to declare it as:
// S <: T means that S is a subtype of T, T is an upper bound of type S
// S >: T would mean that S is a supertype of T
// So, assertAllPos takes a subtype of IntSet and returns it
def assertAllPos(S <: IntSet)(r: S): S
// If assertAllPos threw an exception if the set was empty, we could write:
def assertNotEmptyAndAllPos(S >: NonEmpty <: IntSet)(r: S): S
Covariance
Suppose NonEmpty <: IntSet
. Then List[NonEmpty] <: List[IntSet]
, since a list of Int sets is a special case of a list of arbitrary non-empty sets. So, we call List
a covariant type, because it has a type parameter that allows for subtyping relationships.
For example, arrays in Java are covariant, e.g. NonEmpty <: IntSet
. But this causes problems:
NonEmpty[] a = new NonEmpty{ new NonEmpty(1, Empty, Empty) }
IntSet[] b = a
b[0] = Empty // runtime ArrayStoreException
NonEmpty s = a[0] // Oh no! We're assigning Empty to the NonEmpty var s!
We want line 4 to be impossible, so at runtime when the array is created, Java stores a type tag for a
that says that a
is meant to be an array of NonEmpty
. Then it checks this type tag before assignment on line 3, and throws a ArrayStoreException
so that line 4 is impossible. But this runtime check is inefficient!
We have this design in Java because it allowed for sorting methods that could sort arrays of any type:
// Sorts arrays of all types, because Object is a base type of all types
static void sort(Object[] arr)
// But generics (introduced later) are a better compile-time check
static void sortGeneric[T](T[] arr)
Liskov Substitution Principle ⇒ If A <: B
, then everything one can do with a value of type B
, one should also be able to do with type A
Let’s take a look at the same example from Java, written in Scala. This time, we’d have a type error on line 2, since arrays are not covariant in Scala. Instead, Array
in Scala accepts a generic.
val a: Array[NonEmpty] = Array(new NonEmpty(1, Empty, Empty))
val b: Array[IntSet] = a
b(0) = Empty // notice b(0), because arrays are a form of method call in Scala
val s: NonEmpty = a(0)
As a rule of thumb, if a type is mutable (e.g. Array
) it should not be covariant. If a type is immutable (e.g. List
) it can be covariant.
In general, for a parameterized type C[T]
where A <: B
, there are three possible relationships between C[A]
and C[B]
:
C[A] <: C[B]
⇒ C is covariant, declared asclass C[+A] { ... }
C[A] >: C[B]
⇒ C is contravariant, declared asclass C[-A] { ... }
C[A]
is not a subtype ofC[B]
, and the vice versa also holds ⇒ C is nonvariant, declared asclass C[A] { ... }
For example, if you had two function types:
type A = IntSet => NonEmpty
type B = NonEmpty => IntSet
Then according to the Liskov Substitution principle, A <: B
, since B
’s NonEmpty
parameter would be accepted as A
’s IntSet
parameter and A
’s NonEmpty
return value would be accepted as B
’s IntSet
return value. So, you could substitute any A
for any B
.
In general, if A2 <: A1
and B1 <: B2
, then A1 => B1 <: A2 => B2
. So, functions are contravariant in their argument type (A1 >: A2
) and covariant in their result type (B1 <: B2
).
package scala
trait Function1[-T, +U] {
def apply(x: T): U
}
The Scala compiler makes it impossible to implement a covariant array like in Java through compile time checks:
- covariant type parameters can only appear in method results
- contravariant type parameters can only appear in method parameters
- invariant type parameters can appear anywhere
// A Java covariant array would look something like this
class Array[+T] {
// Impossible, since T is method parameter which is covariant
def update(x: T) // ...
}
Let’s improve the previous List
example using covariance. Recall that we wanted Nil
to be an object, since there is only one empty list. While we’re also at it, let’s also implement a prepend method for lists.
// Covariance is necessary so that List[Nothing] <: List[T]
trait List[+T] {
def isEmpty: Boolean
def head: T
def tail: List[T]
// covariant types can be lower bounds, contravariant types can be upper bounds
def prepend [U >: T] (elem: U): List[U] = new Cons(elem, this)
}
class Cons[T](val head: T, val tail: List[T]) extends List[T] {
def isEmpty = false
}
// use Nothing instead of T because objects cannot have type parameters
object Nil extends List[Nothing] {
def isEmpty = true
// Nothing == Nothing
def head: Nothing = throw new NoSuchElementException("Nil.head")
// Nothing <: List[Nothing]
def tail: Nothing = throw new NoSuchElementException("Nil.tail")
}
object List {
def apply[T](x1: T, x2: T): List[T] = new Cons(x1, new Cons(x2, new Nil))
def apply[T](x: T): List[T] = new Cons(x, new Nil)
def apply[T]() = new Nil
}
val x: List[String] = Nil
// f returns type List[IntSet] since in prepend,
// T would be NonEmpty and U would be empty
// But U >: T is false! So the type inferencer picks the lowest possible U, IntSet
def f(xs: List[NonEmpty], x: Empty) = xs prepend x
Decomposition
Suppose you want to write an interpreter for arithmetic expressions consisting of just numbers and additions. We can represent an expression as a tree of Number
and Sum
objects.
trait Expr {
def isNumber: Boolean
def isSum: Boolean
def numValue: Int
def leftOp: Expr
def rightOp: Expr
}
class Number(n: Int) extends Expr {
def isNumber: Boolean = true
def isSum: Boolean = false
def numValue: Int = n
def leftOp: Expr = throw new Error("Number.leftOp")
def rightOp: Expr = throw new Error("Number.rightOp")
}
class Sum(e1: Expr, e2: Expr) extends Expr {
def isNumber: Boolean = false
def isSum: Boolean = true
def numValue: Int = throw new Error("Sum.numValue")
def leftOp: Expr = e1
def rightOp: Expr = e2
}
def eval(e: Expr): Int = {
if (e.isNumber) e.numValue
else if (e.isSum) eval(e.leftOp) + eval(e.rightOp)
// We could have an expression which is not a number or a sum!
else throw new Error("Unkown expression " + e)
}
Let’s say that we now want to add a class for variables and a class for products. Because of how we’ve implemented Expr
, we’d need new classification methods isProd
and isVar
as well a new accessor name
method. So, to implement Prod
and Var
, we would need 8 _ 5 - 5 _ 3 = 25 new methods.
So, this implementation of Expr
doesn’t scale to new classes. Technically, instead of isProd
and friends we could use the isInstanceOf
check built into Scala, but this is discouraged. To see why, let’s look at this very messy version of eval
.
// Using type tests and type casts, the Expr class could be empty
def eval(e: Expr): Int =
if (e.istanceOf[Number])
e.asInstanceOf[Number].numValue
else if (e.instanceOf[Sum])
eval(e.InstanceOf[Sum].leftOp) +
eval(e.InstanceOf[Sum].rightOp)
else
throw new Error("Unkown expression " + e)
A better solution would be to move the eval
to each class:
trait Expr {
def eval: Int
}
class Number(n: Int) extends Expr {
def eval: Int = n
}
class Sum(e1: Expr, e2: Expr) extends Expr {
def eval: Int = e1.eval + e2.eval
}
But this still has problems due to the limits of object oriented decomposition:
- We have to implement a new method for every class
- We can only write methods local to a single class e.g. we can’t implement the simplification rule ab + ac = a(b + c)
Pattern Matching
Notice that the purpose of test and accessor functions is to recover the construction process. For instance, when we create new Sum(e1, e2)
, test functions check for the subclass created (Sum
) and accessor functions check for the arguments passed (e1, e2
).
Scala automates this process by use of pattern matching, which is switch statement built for hierarchies. Patterns can be any combination of:
- constructors e.g.
Number, Sum
- variables, must start with a lowercase letter and only appear once per pattern e.g.
n, e1, e2
- wildcard patterns e.g.
Number(_)
- constants, must either be a literal or start with an uppercase letter e.g.
1, true, "abc", PI
trait Expr {
// Now we can pull out the construction args using pattern matching
// This eval only deals with Number and Sum
def eval(e: Expr): Int = e match {
case Number(n) => n
case Sum(e1, e2) => e1.eval + e2.eval
// MatchError exception if no pattern matches e
}
}
case class Number(n: Int) extends Expr
case class Sum(e1: Expr, e2: Expr) extends Expr
case class Var(x: String) extends Expr
case class Prod(e1: Expr, e2: Expr) extends Expr
// the case keyword automatically creates these companion objects
object Number {
def apply(n: Int) = new Number(n) // Number(1) === new Number(1)
}
object Sum {
def apply(e1: Expr, e2: Expr) = new Sum(e1, e2)
}
object Var // ...
object Prod // ...
// Let's see an example evaluation of eval
Sum(Number(1), Number(2).eval
=== Sum(Number(1), Number(2)) match {
case Number(n) => n
case Sum(e1, e2) => e1.eval + e2.eval
}
=== Number(1).eval + Number(2).eval
=== Number(1) match {
case Number(n) => n
case Sum(e1, e2) => e1.eval + e2.eval
} + Number(2).eval
=== 1 + Number(2).eval
=== 1 + Number(2) match {
case Number(n) => n
case Sum(e1, e2) => e1.eval + e2.eval
}
=== 1 + 2
=== 3
// We can use pattern matching inside or outside of classes
def show(e: Expr): String = {
def sumBrackets(ex: Expr): String = ex match {
case Sum(e1, e2) => "(" + show(ex) + ")"
case _ => show(ex) // default
}
e match {
case Number(n) => n.toString
case Sum(e1, e2) => show(e1) + " + " + show(e2)
case Prod(e1, e2) => sumBrackets(e1) + " * " + sumBrackets(e2)
case Var(x) => x
}
}
Lists
There are two important differences between arrays and lists:
- Lists are immutable
-
Lists are recursive (think cons lists), while arrays are flat
val l: List[String] = List("a", "b", "c") /* l is a cons list: [] | \ a [] | \ b [] | \ c Nil */
All lists are constructed from the empty list Nil
and the construction operation ::
.
val l = List("a", "b", "c")
val m = "a" :: ("b" :: ("c" :: Nil))
l === m
// all operators ending in : associate to the right
val n = "a" :: "b" :: "c" :: Nil
l === m === n
// all operators ending in : are seen as method calls of the right hand operand
val o = Nil.::(4).::(3).::(2).::(1)
l === l === n === o
// so :: can be thought of as a prepend method
All operations on lists can be expressed in terms of the following 3 operations:
head
⇒ the first element of the listtail
⇒ the list composed of all elements except the firstisEmpty
⇒ true if the list is empty, false otherwise
We can decompose lists with pattern matching. Some examples of valid patterns:
Nil
⇒ the empty listp :: ps
⇒ the list with headp
and tailps
, whereps
can beNil
List(p1, ... pn)
⇒ the list with headp1
ending with elementpn
// Insertion sort
def isort(xs: List[Int]): List[Int] = xs match {
case List() => List()
case y :: ys => insert(y, isort(ys))
}
def insert(x: Int, xs: List[Int]): List[Int] = xs match {
case List() => List(x)
case y :: ys => if (x <= y) x :: xs else y :: insert(x, ys)
}
Week 5
More Functions on Lists
Useful methods for List
xs.length
⇒ the number of elements ofxs
xs.last
⇒ the list’s last element, exception ifxs
is emptyxs.init
⇒ all the elements ofxs
except the last, exception ifxs
is emptyxs take n
⇒ firstn
elements ofxs
, orxs
ifxs
has length less thann
xs drop n
⇒ rest ofxs
after takingn
elementsxs(n) === xs.apply(n)
⇒ the element ofxs
at indexn
xs ++ ys === xs ::: ys === ys.:::(xs)
⇒ the listxs
concatenated with the listys
xs.reverse
⇒ the listxs
in reverse orderxs.updated(n, x)
⇒ the listxs
with the element at indexn
replaced byx
xs indexOf x
⇒ the index of the first element inxs
equal tox
, or-1
if not foundxs contains x
⇒ true ifx
inxs
, false otherwise
Let’s analyze the complexity of some of these methods by writing possible implementations.
// O(N)
def last[T](xs: List[T]): T = xs match {
case List() => throw new Error("last of empty list")
case List(x) => x
case y :: ys => last(ys)
}
// O(N)
def init[T](xs: List[T]): List[T] = xs match {
case List() => throw new Error("init of empty list")
case List(x) => List()
case y :: ys => y :: init(ys)
}
// O(|xs|)
def concat[T](xs: List[T], ys: List[T]) = xs match {
case List() => ys
case z :: zs => z :: concat(zs, ys)
}
// O(N^2) => reverse(ys) takes 1 + 2 + ... + N = O(N) steps, and we iterate N times
def reverse[T](xs: List[T]): List[T] = xs match {
case List() => List()
case y :: ys => reverse(ys) ::: List(y)
}
def removeAt[T](xs: List[T], n: Int): List[T] = (xs take n) ::: (xs drop n + 1)
def flatten[T](xs: List[T]): List[T] = xs.foldLeft(List()) {
case (acc, ls: List[T]) => acc ::: flatten(ls)
case (acc, x) => acc ::: List(x)
}
Pairs and Tuples
Let’s see a more efficient sort than insertion sort, merge sort, to illustrate tuples.
// O(NlogN)
def mergesort(xs: List[Int]): List[Int] = {
val n = xs.length / 2
if (n == 0) xs
else {
// O(N) merge
def merge(xs: List[Int], ys: List[Int]) = (xs, ys) match {
case (List(), _) => ys
case (_, List()) => xs
case (x :: xt, y :: yt) =>
if (x < y) x :: merge(xt, ys)
else y :: merge(xs, yt)
}
// tuple pattern binding
val (fst, snd) = xs splitAt n
merge(mergesort(fst), mergesort(snd))
}
}
Implicit Parameters
Let’s parameterize mergesort
so that it can be used with any type T
. We’ll need to pass a comparison function for T
.
// pass a less than function
def mergesort[T](xs: List[T])(lt: (T, T) => Boolean): List[T] = {
val n = xs.length / 2
if (n == 0) xs
else {
// O(N) merge
def merge(xs: List[T], ys: List[T]) = (xs, ys) match {
case (List(), _) => ys
case (_, List()) => xs
case (x :: xt, y :: yt) =>
if (lt(x, y)) x :: merge(xt, ys)
else y :: merge(xs, yt)
}
// tuple pattern binding
val (fst, snd) = xs splitAt n
merge(mergesort(fst)(lt), mergesort(snd)(lt))
}
val nums = List(1, 2, 3, -4)
val fruits = List("a", "d", "c", "b")
// we could leave out type in anonymous comparison functions
mergesort(nums)((x: Int, y: Int) => x < y)
mergesort(fruits)((x: String, y: String) => x.compareTo(y) < 0)
Instead of writing our own, we could instead use scala.math.Ordering[T]
. This allows us to use the implicit
keyword, which makes the compiler automatically find an implicit ordering definition which:
- is marked
implicit
- has a type compatible with
T
- is visible to the function or is defined in a companion object of
T
import math.Ordering
def mergesort[T](xs: List[T])(implicit ord: Ordering[T]): List[T] = {
val n = xs.length / 2
if (n == 0) xs
else {
// O(N) merge
def merge(xs: List[T], ys: List[T]) = (xs, ys) match {
case (List(), _) => ys
case (_, List()) => xs
case (x :: xt, y :: yt) =>
if (ord.lt(x, y)) x :: merge(xt, ys)
else y :: merge(xs, yt)
}
// tuple pattern binding
val (fst, snd) = xs splitAt n
// since we have implicit ord, we don't need to pass ord
merge(mergesort(fst), mergesort(snd))
}
val nums = List(1, 2, 3, -4)
val fruits = List("a", "d", "c", "b")
mergesort(nums)(Ordering.Int)
mergesort(fruits)(Ordering.String)
// implicit keyword means that compiler finds the correct ordering based on type
mergesort(nums)
mergesort(fruits)
Higher Order List Functions
Common operations on lists:
- transforming each element in a certain way
- filtering the list based on some criterion e.g.
xs filter p
⇒ all elements ofxs
satisfyingp
xs filterNot p
⇒ all elements ofxs
satisfying!p
xs partition p
⇒ the pair(xs filter p, xs filterNot p)
xs takeWhile p
⇒ the longest prefix ofxs
consisting of elements that satisfyp
xs dropWhile p
⇒ the remainder ofxs
without any leading elements satisfyingp
xs span p
⇒ the pair(xs takeWhile p, xs dropWhile p)
- combining the elements of the list using some operator
Let’s write (simplified) generic versions of these operations.
abstract class List[T] {
// Real implementations of these are more complex
// They would be tail recursive and work on other collections
def map[U](f: T => U): List[U] = this match {
case Nil => this
case x :: xs => f(x) :: xs.map(f)
}
def filter(p: T => Boolean): List[T] = this match {
case Nil => this
case x :: xs => if (p(x)) x :: xs.filter(p) else xs.filter(p)
}
def reduceLeft(op: (T, T) => T): T = this match {
case Nil => throw new Error("Nil.reduceLeft")
case x :: xs => (xs foldLeft x)(op) // acc is the first element, x
}
// apply op and fold into z for x1, x2 ... xN
def foldLeft[U](z: U)(op: (U, T) => U): U = this match {
case Nil => z
case x :: xs => (xs foldLeft op(z, x))(op)
}
def reduceRight(op: (T, T) => T): T = this match {
case Nil => throw new Error("Nil.reduceRight")
case x :: Nil => x
case x :: xs => op(x, xs.reduceRight(op))
}
// apply op and fold into z for xN,... x2, x1
def foldRight[U][x: U](op: (T, U) => U): U = this match {
case Nil => z
case x :: xs => op(x, (xs foldRight z)(op))
}
}
// We can use generic functions in other functions like this:
def scaleList(xs: List[Double], factor: Double): List[Double] =
xs map (x => x * factor)
def squareList(xs: List[Int]): List[Int] =
xs map (x => x * x)
def posElems(xs: List[Int]): List[Int] =
xs filter (x => x > 0)
// we can't put foldLeft here because we can only apply :: on List(T)
def concat[T](xs: List[T], ys: List[T]): List[T] =
(xs foldRight ys)(_ :: _)
def mapFun[T, U](xs: List[T], f: T => U): List[U] =
(xs foldRight List[U]())((x, acc) => f(x) :: acc)
def lengthFun[T](xs: List[T]): Int =
(xs foldRight 0)( (_, acc) => 1 + acc )
As an exercise, let’s write a function pack
that packs consecutive duplicates of list elements into sublists. Then, let’s use pack
to write encode
, which returns the run length encoding of a list.
def pack[T](xs: List[T]): List[List[T]] = xs match {
case Nil => Nil
case x :: xt => {
val (prefix, remainder) = xs span (e => e == x)
prefix :: pack(remainder)
}
val data = List("a", "a", "b", "c", "c", "a")
pack(data) // List(List("a", "a", "a"), List("b"), List("c", "c"), List("a"))
def encode[T](xs: List[T]): List[(T, Int) =
pack(xs).map(ys => (ys.head, ys.length))
encode(data) // List((a, 3), (b, 1), (c, 2), (a, 1))
We’d like to prove that concat
satisfies the following properties through structural induction:
- associative ⇒
(xs ++ ys) ++ zs === xs ++ (ys ++ zs)
- admits
Nil
as a neutral element to the left and right of a list ⇒xs ++ Nil === xs === Nil ++ xs
Recall natural induction consists of a base case and a induction step. For example, to show a property P(n) for all integers n ≥ b, we must:
- Show P(b)
- Show that for all integers n ≥ b, if P(n) then P(n + 1)
referential transparency ⇒ a term is equivalent to the term to which it reduces, holds in pure functional programming since there are no side effects
Structural induction has a similar structure to natural induction. To prove a property P(xs
) holds for all lists xs
:
- Show P(
Nil
) - Show that for a list
xs
and some elementx
, if P(xs
) then P(x :: xs
)
Let’s prove that concat
is associative first. Recall the definition of concat
:
def concat[T](xs: List[T], ys: List[T]) = xs match {
case List() => ys
case x :: xt => x :: concat(xt, ys)
}
From the implementation of concat, we can see 2 defining clauses:
Nil ++ ys === ys
(x :: xt) ++ ys === x :: (xt ++ ys)
Recall that the definition of association is (xs ++ ys) ++ zs === xs ++ (ys ++ zs)
. For the base case, xs = Nil
. Let’s simplify using our first defining clause to see that association holds in the Nil
case.
(xs ++ ys) ++ zs === (Nil ++ ys) ++ zs // by 1st defining clause
=== ys + zs
xs ++ (ys ++ zs) === Nil ++ (ys ++ zs) // by 1st defining clause
=== ys + zs
// thus, for xs => Nil,
(xs ++ ys) ++ zs === xs ++ (ys ++ zs)
Now let’s prove association in the induction step, x :: xs
.
(xs ++ ys) ++ zs === ((x :: xs) ++ ys) ++ zs
=== (x :: (xs ++ ys)) ++ zs // by 2nd defining clause
=== x :: ((xs ++ ys) ++ zs)) // by 2nd defining clause
=== x :: (xs ++ (ys ++ zs)) // by induction hypothesis
xs ++ (ys ++ zs) === (x :: xs) ++ (ys ++ zs)
=== x :: (xs ++ (ys ++ zs)) // by 2nd defining clause
// thus, for xs => x :: xs,
(xs ++ ys) ++ zs === xs ++ (ys ++ zs)
Thus, we have proven association on concat
by structural induction.
Next, let’s prove that concat
admits Nil
as a neutral element on the right, meaning that xs ++ Nil === xs
.
For the base case, xs => Nil
.
xs ++ Nil === Nil ++ Nil
=== Nil // by 1st defining clause
=== xs
For the induction step, xs => x :: xs
.
(x :: xs) ++ Nil
=== x :: (xs ++ Nil) // by 2nd defining clause
=== x :: xs // by induction hypothesis
Let’s now see a more complex proof, for the reverse function.
def reverse[T](xs: List[T]): List[T] = xs match {
case List() => List()
case y :: ys => reverse(ys) ::: List(y)
}
We see the 2 following defining clauses:
Nil.reverse = Nil
(x :: xs).reverse = xs.reverse ++ List(x)
We’d like to prove that xs.reverse.reverse = xs
.
For the base case, xs => Nil
.
Nil.reverse.reverse
=== Nil.reverse
=== Nil
For the induction step, xs => x :: xs
.
(x :: xs).reverse.reverse
=== (xs.reverse ++ List(x)).reverse
// Let ys => xs.reverse
=== (ys ++ List(x)).reverse
x :: xs
=== x :: xs.reverse.reverse // by induction hypothesis
=== x :: ys.reverse
// Let's now do structural induction on ys, to prove
(ys ++ List(x)).reverse === x :: ys.reverse
// Base case: ys => Nil
(Nil ++ List(x)).reverse
=== List(x).reverse // 1st clause of ++
=== (x :: Nil).reverse // definition of List
=== Nil.reverse ++ List(x) // 2nd clause of reverse
=== Nil ++ (x :: Nil) // 1st clause of reverse, definition of list
=== x :: Nil // 1st clause of ++
=== x :: Nil.reverse // 1st clause of reverse
// Induction step: ys => y :: ys
((y :: ys) ++ List(x)).reverse
=== (y :: (ys ++ List(x))).reverse // 2nd clause of ++
=== (ys ++ List(x)).reverse ++ List(y) // 2nd clause of reverse
=== (x :: ys.reverse) ++ List(y) // induction hypothesis
=== x :: (ys.reverse ++ List(y)) // first clause of ++
=== x :: (y :: ys).reverse // 2nd clause of reverse
Let’s now prove a distribution law for map over concatenation. For any lists xs, ys
and function f
, (xs ++ ys) map f = (xs map f) ++ (ys map f)
.
Assume we have the following defining clauses for map:
Nil map f = Nil
(x :: xs) map f = f(x) :: (xs map f)
For the base case:
// xs => Nil
(Nil ++ ys) map f
=== ys map f // 1st clause of ++
=== Nil ++ ys map f // by 1st clause of ++
=== (Nil map f) ++ ys map f // 1st clause of map
// ys => Nil
(xs ++ Nil) map f
=== xs map f
=== xs map f ++ Nil
=== xs map f ++ Nil map f
For the induction step:
// xs => x :: xs
((x :: xs) ++ ys) map f
=== (x :: (xs ++ ys)) map f // 2nd clause of ++
=== f(x) :: ((xs ++ ys) map f) // 2nd clause of map
=== f(x) :: ((xs map f) ++ (ys map f)) // induction hypothesis
=== (f(x) :: (xs map f)) ++ (ys map f) // 2nd clause of ++
=== ((x :: xs) map f) ++ (ys map f) // 2nd clause of map
// ys => y :: ys ??? can't do this proof
Week 6
Sequences
Vector
⇒ an immutable data structure with a more evenly balanced access pattern than List
- For up to 32 elements, an
Array
- For more than 32 elements, a tree where each node is an
Array
with 32 pointers and each leaf is anArray
with 32 elements
Random access for a Vector
is O(logN), since the depth of the Vector
increases at logN. We pick 32 since 32 elements fits on a single cache line on most platforms, which gives us space/time locality for operations on elements in bulk e.g. map, filter, reduce.
Random access for List
is O(N) and there are no guarantees on where the data for List
objects will be stored on disk. So, we should prefer List
to Vector
when we need patterns of repeatedly accessing head
and prefer Vector
to List
when doing bulk operations.
val nums = Vector(1, 2, 3, -88)
val people = Vector("Bob", "James", "Peter")
// Vectors support all List operations (both subclass Seq, Iterable)
// However, instead of x :: xs, vectors have:
0 +: nums // Vector(0, 1, 2, 3, -88)
people :+ "Martin" // Vector("Bob", "James", "Peter", "Martin")
Appending elements causes the relevant inner nodes in the Vector
to be replaced, which is an O(logN) operation since the depth of the Vector
increases at logN.
// Arrays and Strings also support Seq operations, but do not subclass Seq
// since they come from Java
val xs = Array(1, 2, 3, 44)
xs map (x => x * 2)
val s = "Hello World"
s filter (c => c.isUpper)
Range
⇒ Seq
of evenly spaced integers with operators to
(inclusive), until
(exclusive), and by
(determines step value)
// Range only stores max, min, and step value
val r: Range = 1 until 5 // 1, 2, 3, 4
val s: Range = 1 to 5 // 1, 2, 3, 4, 5
1 to 10 by 3 // 1, 4, 7, 10
More sequence operations:
xs exists p
⇒ true if there is some x in xs such that p(x) holds, else falsexs forall p
⇒ true if p(x) holds for all xs, else falsexs zip ys
⇒ sequence of pairs drawn from xs and ys until the shorter list is exhaustedxs.unzip
⇒ splits sequence of pairsxs
into two sequencesxs.flatMap f
⇒ applies f to all elements of xs, concatenates, and then flattens
val s = "Hello World"
s flatMap (c => List('.', c)) // ".H.e.l.l.o. .W.o.r.l.d"
s.flatten
==== s foldRight "")(_ ++ _)
==== s flatMap (x => x)
xs.sum
⇒ sum of all elements of the numeric list xsxs.product
⇒ product of all elements of the numeric list xsxs.min
⇒ minimum of xs (Ordering must exist)xs.max
⇒ maximum of xs (Ordering must exist)
Examples
-
Write a function to list all combinations of numbers x and y where x is drawn from the range 1 … M and y is drawn from the range 1 … N.
(1 to M) flatMap ( x => (1 to N) map (y => (x, y)))
-
Compute the scalar product of two vectors
def scalarProduct(xs: Vector[Double], ys: Vector[Double]): Double = (xs zip ys).map(xy => xy._1 * xy._2).sum // Or, use pattern matching def scalarProductPM(xs: Vector[Double], ys: Vector[Double]): Double = (xs zip ys).map{ case (x, y) => x * y }.sum
-
Write a high level way to test for primality of numbers.
def isPrime(n: Int): Boolean = (2 to n) forall (d => n % d != 0)
-
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.
// generate all integers i between 1 and n (1 until n) flatMap (i => // generate the list of pairs (i, 1), (i, 2), ... (i, i - 1) (1 until i) map (j => (i, j))) // produces a IndexedSequence: [(2, 1), (3, 1), (3, 2), ...] filter{ case (i, j) => isPrime(i + j) }
Let’s now look to simplify this using the
for
expression, which produces a result list over iterations without side effects.// For example, these are equivalent for (p <- persons if p.age > 20) yield p.name === persons filter (p => p.age > 20) map (p => p.name)
for
expressions have the general formfor (p <- c if f) yield e
:p <- c
⇒ generator where p is a pattern and c is a collectionif f
⇒ filter
for
expressions can have multiple generators, where each sequential generator varies faster than the last.for { i <- 1 until n j <- 1 until i if isPrime(i + j) } yield (i, j)
-
Write a version of
scalarProduct
that makes use of afor
.def scalarProduct(xs: List[Double], ys: List[Double]): Double = (for ((x, y) <- xs zip ys) yield x * y).sum
Sets
// Most operations on Sequences are also available on Sets
val fruit = Set("a", "b", "c")
val s = (1 to 6).toSet
Principal differences between Set
and Sequence
:
- Sets are unordered
- Sets do not have duplicate elements
- The fundamental operation on sets is
contains
Recall the N-Queens problem. We want to place N queens on a N by N chessboard such that no two queens are on the same row, column, or diagonal. Assume each solution is represented of a list of length N, where the index is the row of the queen and the value is column of the queen, and the queens are listed from the bottommost row up. We can solve this problem with a recursive algorithm.
object nqueens {
def queens(n: Int): Set[List[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)
}
def isSafe(col: Int, queens: List[Int]): Boolean
val row = queens.length
val queensWithRow = (row - 1 to 0 by -1) zip queens
queensWithRow forall {
case (r, c) => col != c && math.abs(col - c) != row - r
}
def show(queens: List[Int]) = {
val lines =
for (col <- queens.reverse)
yield Vector.fill(queens.length)("* ").updated(col, "X ").mkString
"\n" + (lines mkString "\n")
}
}
Maps
val romanNumerals = Map("I" -> 1, "V" -> 5, "X" -> 10) // Map[String, Int]
val capitalOfCountry = Map("US" -> "Washington", "Switzerland" -> "Bern")
// Maps are iterables, Map[K, V] extends Iterable[(K, V)]
val countryOfCapital = capitalOfCountry map {
case (x, y) => (y, x)
}
// Maps are also functions, Map[K, V] extends K => V
capitalOfCountry("US") // "Washington"
capitalOfCountry("Andorra") // NoSuchElementException
capitalOfCountry get "Andorra" // Option[String] = None
captialOfCountry get "US" // Option[String] = Some("Washington")
val capitals = capitalOfCountry withDefaultValue "unknown"
capitals("Andorra") // "unknown
// Option definition
trait Option[+A]
case class Some[+A](value: A extends Option[A]
object None extends Option[Notion]
// Since Options are case classes, they can be pattern matched
def showCapital(country: String) = captialOfCountry.get(country) match {
case Some(capital) => capital
case None => "missing data"
}
// Options also support map, filter, etc.
Let’s now look at groupBy
and orderBy
, which are two operations from SQL we can express in for
expressions.
// orderBy => sortWith and sorted
val fruit = List("apple", "pear", "orange", "pineapple")
fruit sortWith (_.length < _.length) // sort by word length
fruit.sorted // lexographic
// groupBy
fruit groupBy (_.head)
// Map("p" -> List("pear", "pineapple"),
// "a" -> List("apple"),
// "o" -> List("orange"))
Let’s design a class Poly
that represents a polynomial as a map from each exponent to its coefficient.
class Poly(val terms0: Map[Int, Double]) {
// allow us to pass a sequence for construction
def this(bindings: (Int, Double)*) = this(bindings.toMap)
val terms = terms0 withDefaultValue(0.0)
// When 2 maps are added, matching keys are overwritten in the first map
// So, we need to adjust the second map
def + (other: Poly) = new Poly(terms ++ (other.terms map adjust))
def adjust(term: (Int, Double)): (Int, Double) = {
val (exp, coeff) = term
exp -> (coeff + terms(exp))
}
// An faster implementation using foldLeft since we don't have to
// store an intermediate list of adjusted terms
def + (other: Poly) = new Poly((other.terms foldLeft terms)(addTerm)
def addTerm(terms: Map[Int, Double], term: (Int, Double)): Map[Int, Double] = {
val (exp, coeff) = term
terms + exp -> (coeff + terms(exp))
}
override def toString =
(for ((exp, coeff) <- terms.toList.sorted.reverse)
yield coeff + "x^" + exp
) mkString " + "
}
val p1 = new Poly(1 -> 2.0, 3 -> 4.0, 5 -> 6.2)
val p2 = new Poly(Map(0 -> 3.0, 3 -> 7.0))
p1 + p2
p1.terms(7) // 0.0
Let’s now see a larger example. Phone keys have mnemonics assigned to them:
val mnem = Map(
'2' -> "ABC", '3' -> "DEF", '4' -> "GHI", '5' -> "JKL", '6' -> "MNO",
'7' -> "PQRS", '8' -> "TUV", '9' -> "WXYZ"
)
Design a method translate(phoneNumber)
that produces all possible words that phoneNumber
could represent.
val in = Source.fromURL(...) // get dictionary from source
val words = in.getLines.toList filter (w => w forall (c => c.isLetter))
val mnem = ...
// Invert the mnem map to give a map from chars A...Z to 2...9
val charCode: Map[Char, Char] = for((digit, str) <- mnem; l <- str) yield ltr -> digit
// Maps a word to the difit string it can represeng e.g. Java -> 5282
def wordCode(word: String): String = word.toUpperCase map charCode
/*
* A map from digit strings to the words that represent them
* e.g. 5282 -> List("Java", "Kata", "Lava", ...)
* Note: A missing number should map to the empty set e.g. "1111" -> List()
*/
val wordsForNum: Map[String, Seq[String]] =
words groupBy wordCode withDefaultValue Seq()
// Return all ways to encode a number as a list of words
def encode(number: String): Set[List[String]] =
if (number.isEmpty) Set(List())
else {
for {
split <- 1 to number.length
word <- wordsForNum(number take split)
rest <- encode(number drop split)
} yield word :: rest
}.toSet
// Get phrases set
def translate(number: String): Set[String] =
encode(number) map (_ mkString " ")