What we will learn in this lecture
Introduction
Immutability
Algebraic Data Types
Pure Functions
Referential Transparency
Recursion
Composability
Definition
FP is a declarative programming paradigm which tries to structure code as expressions and declarations. Thus, we end up with pure functions, no side-effects or shared mutable state.
Who is it coming from
The basic ideas stem from Lambda Calculus which was discovered by Alonzo Church in the 1930s.
What we will discuss
We will concentrate on typed Functional programming fulfilling all properties.
What other people/languages do
Other languages/people might just choose a subset.
Immutability: definition
Data never changes.
Data never changes
// you cannot reassign to `a`
val a = 5
// you cannot mutate fields in a clase class instance
case class Person(name: String, age: Int)
val gandalf = Person("Gandalf", 2019)
gandalf.name = "Gandolf" // not allowed
Data never changes
state of your values known at all times
no race-conditions in a concurrent scenario
simplifies reasoning about values in your code
ADT: definition
They are compositions of types and resemble algebraic operations.
ADT: Sum types
Sum Type: value types are grouped into specific classes.
// exists a value or not
sealed trait Animal
case object Mammal extends Animal
case object Bird extends Animal
case object Fish extends Animal
ADT: sum types
Product Type: a single class combining multiple value types
case class Pair(a: Int, b: String)
ADT: mix and match
// already know this construct
// Person is a sum type
sealed trait Person
// each class is a product type
case class Wizard(name: String, power: String) extends Person
case class Elf(name: String, age: Int) extends Person
case class Dwarf(name: String, height: Int) extends Person
ADT
We already know these constructs. Now we have a name for them.
Pure Functions: definition
A function is:
total: returns an output for every input
deterministic: returns the same output for the same input
pure: only computes the output, doesn't effect the "real world"
Pure Functions: in a mathematical sense
$f : a \rightarrow b$
Pure Functions
def plus(a: Int, b: Int): Int = a + b
// works for every input and always returns the same output
plus(1, 2) == 3
Impure Functions: exceptions
def divide(a: Int, b: Int): Int = a / b
// throws an ArithmeticExceptions which bypasses your call stack
divide(1, 0) == ???
Exceptions
A Java construct which bypasses your function applications and crashes a program if not handled.
We try to avoid them at all costs.
Impure Functions: partial functions
// partial function
def question(q: String): Int = q match {
case "answer to everything" => 42
}
// throws an Exceptions which bypasses your call stack
question("is the sun shining") == ???
Impure Functions: non-determinism
// non-determinism
def question(q: String): Int = Random.nextInt()
question("what is the answer to everything") == 42
question("what is the answer to everything") == 136
question("what is the answer to everything") == 5310
Impure Functions: interacts with "the real world"
// reads file from disk
def readFile(path: String): Either[Exception, File] = ???
// it might return a file handle
readFile("/usr/people.cvs") == Right(File(...))
// or fail, e.g. file is missing, not enough rights, ...
readFile("/usr/people.cvs") == Left(FileNotFoundException)
Impure Functions: interacts with "the real world"
// writes file to disk
def writeFile(file: File): Either[Exception, Unit] = ???
// changes the state of your machine aka real world
val file = new File(...)
writeFile(file)
Impure Functions: interacts with "the real world"
// even logging is changing the world
def log(msg: String): Unit = println(msg)
log("hello world")
Impure Functions: effect "the real world"
Application Input/Output (IO) is not allowed. How to write useful programs then?
We will discuss that later!
Is this function pure?
def multiply(a: Int, b: Int): Int = a * b
// yes
def concat(a: String, b: String): String = {
println(a)
a + b
}
// no -> we mutate the OS by writing to System Out
Is this function pure?
def whatNumber(a: Int): String = a > 100 match {
case true => "large one"
}
// no -> partial function, only handles the `true` case
def increase(a: Int): Int = {
// current time in milliseconds
val current = System.currentTimeMillis
a + current
}
// no -> result depends on the time we call the function
makes it easy to reason about code
separates business logic from real world interaction
Referential Transparency: definition
An expression is referential transparent if you can replace it by its evaluation result without changing the programs behavior. Otherwise, it is referential opaque.
Referential Transparency
no side-effects
same output for the same input
execution order doesn't matter
Referential Transparency: pure functions
Referential Opaque
We already know what effectful and non-deterministic functions look like
But what is with the execution order?
Referencial Opaque: variables and execution order
// defines a variable - a mutable (imperative) reference
var a = 1
a == 1
a = a + 1
a == 2
a = a + 1
a == 3
makes it easy to reason about code
separates business logic from real world interaction
Recursion: definition
Solving a problem where the solution depends on solutions to smaller instances of the same problem.
Recursion: data types
Definition of the data structures depends on itself.
Recursion: List
// linked list of Ints
sealed trait IntList
// a single list element with its Int value and the remaining list
// this class of IntList is defined by using IntList (tail)
case class Cons(head: Int, tail: IntList) extends IntList
// end of the list
case object Nil extends IntList
val list = Cons(0, Cons(1, Cons(2, Nil)))
Recursion: List
Recursion: direct single
Type Parameter: ADT
sealed trait List[A]
// ^
// '
// type parameter
case class Cons[A](head: A, tail: List[A]) extends List[A]
// ^ ^ ^ ^
// ' '---- '----------------|
// type parameter ' |
// fixes type of our value |
// '
// fixes type of remaing/whole list
case class Nil[A]() extends List[A]
Type Parameter: ADT
val intList = Cons[Int](0, Cons[Int](1, Cons[Int](2, Nil[Int]())))
val charList = Cons[Char]('a', Cons[Char]('b', Nil[Char]()))
// Scala can infer `A` by looking at the values
val intList = Cons(0, Cons(1, Cons(2, Nil())))
// Scala knows that `0: Int`
// '- List[A] ~ List[Int]
val charList = Cons('a', Cons('b', Nil()))
// Scala knows that `'a': Char`
// '- List[A] ~ List[Char]
Type Parameter: ADT
also called generics
can be fixed by hand: Cons[Int](0, Nil[Int]): List[Int]
or inferred by Scala: Cons(0, Nil): List[Int]
Let's Code: RecursiveData
sbt> project fp101-exercises
sbt> test:testOnly exercise2.RecursiveDataSpec
Recursion: functions
Functions which call themselves.
Recursion: example
//factorial
def fact(n: Int): Int = n match {
case 1 => 1
case _ => n * fact(n - 1)
}
fact(3)
// '- 3 * fact(2)
// '- 2 * fact(1)
// '- 1
fact(3) == 3 * fact(2)
== 3 * 2 * fact(1)
== 3 * 2 * 1
== 6
Recursion: direct single
def length(list: List[Int]): Int = list match {
// final state
case Nil() => 0
// single direct recusive step (length calls itself)
case Cons(_, tail) => 1 + length(tail)
}
Recursion: direct single
val list = Cons(1, Cons(2, Nil()))
length(list) == 1 + length(Cons(2, Nil()))
== 1 + 1 + length(Nil())
== 1 + 1 + 0
== 2
Recursion: Tree
/* Tree: either a leaf with a value or a node consisting of a
* left and right tree
*/
def size(tree: Tree[Int]): Int = tree match {
// final state
case Leaf(_) => 1
// multiple direct recusive steps (branches into two recursice calls)
case Node(left, right) => size(left) + size(right)
}
Recursion: Tree
val tree = Node(Node(Leaf(1), Leaf(2)), Leaf(3))
size(tree) == size(Node(Leaf(1), Leaf(2))) + size(Leaf(3))
== size(Leaf(1)) + size(Leaf(2)) + 1
== 1 + 1 + 1
== 3
Recursion: direct multi
Recursion: even-odd
def even(n: Int): Boolean =
if (n == 0) true
else odd(n - 1)
def odd(n: Int): Boolean =
if (n == 0) false
else even(n - 1)
Recursion: even-odd
even(5) == odd(4)
== even(3)
== odd(2)
== even(1)
== odd(0)
== false
Recursion: indirect
Structural Recursion
When you consume a (recursive) data structure which gets smaller with every step, e.g. length of a list. This kind of recursion is guaranteed$^*$ terminate.
Generative Recursion
Generates a new data structure from its input and continues to work on it. This kind of recursion isn't guaranteed to terminate.
Generative Recursion
// this function will never stop
def stream(a: Int): List[Int] = Cons(a, stream(a))
What kind of recursion is it?
def plus(n: Int, m: Int): Int = n match {
case 0 => m
case _ => 1 + plus(n - 1, m)
}
// structural direct single
What kind of recursion is it?
def produce(n: Int): List[Int] = n match {
case 0 => Nil()
case _ => Cons(n, produce(n))
}
// generative direct single
What kind of recursion is it?
def parseH(str: List[Char]): Boolean = str match {
case Cons('h', tail) => parseE(tail)
case _ => false
}
def parseE(str: List[Char]): Boolean = str match {
case Cons('e', tail) => parseY(tail)
case _ => false
}
def parseY(str: List[Char]): Boolean = str match {
case Cons('y', Nil()) => true
case Cons('y', tail) => parseH(tail)
case _ => false
}
// structural indirect multi
Recursion: types
Let's summaries the different recursion types we have seen so far.
Recursion: types
single/multi recursion
direct/indirect recursion
structural/generative recursion
Recursion: single/multi direct
Recursion: indirect
What are possible problems
How to fix types of Generics?
What is the impact of recursion on the runtime behaviour?
Type Parameter: functions
Again, do we need to write a function for every `A` in a Generic?
No! type parameters to the rescue.
Type Parameters: functions
def length[A](list: List[A]): Int = list match {
// ^ ^
// ' '---------
// type parameter '
// fixes list type `A`
case Nil() => 0
case Cons(_, tail) => 1 + length[A](list)
}
Type Parameters: functions
length[Int](Cons(1, Cons(2, Nil()))) == 2
// or we rely on inference again
length(Cons(1, Cons(2, Nil()))) == 2
// Scala knows that `1: Int`
// '- List[A] ~ List[Int]
// '- length[A] ~ length[Int]
Recursion: runtime impact
How do multiple recursive steps affect the runtime behaviour?
Recursion: call stack
length(Cons(0, Cons(1, Nil())))
// '- 1 + length(Cons(1, Nil()))
// '- 1 + length(Nil())
// '- 0
Recursion: call stack
Recursion: call stack
But what happens if the list is reaaaaally long?
Your program will run out of memory (stack overflow)
Recursion: tail recursion
If a function has a single, direct recursion and the last expression is the recursive call, it is stacksafe.
Recursion: tail recursion
def lengthSafe[A](list: List[A]): Int = list {
def loop(remaining: List[A], accu: Int): Int = remaining match {
case Nil() => accu
case Cons(_, tail) => loop(tail, accu + 1)
// ^
// '
// last expression
}
loop(list, 0)
}
lengthSafe(Cons(0, Cons(1, Nil()))) == 2
Recursion: tail recursion
Scala optimizes this function to an imperative loop. Therefore, the stack does not grow.
Recursion: tail recursion
def lengthSafe[A](list: List[A]): Int = list {
// Scala now checks if this function is tail recursive
@tailrec
def loop(remaining: List[A], agg: Int): Int = ???
loop(list, 0)
}
Is this function stack-safe?
def fib(n: Int): Int = n match {
case 1 | 2 => 1
case _ => fib(n - 1) + fib(n - 2)
}
/* no, last expression is the `+` operator and we
* create two recursion branches
*/
Is this function stack-safe?
def last[A](list: List[A]): List[A] = list match {
case el@ Cons(_, Nil()) => el
case Cons(_, tail) => last(tail)
}
// yes, last expression is `last`
Is this function stack-safe?
def size[A](tree: Tree[A]): Int = tree match {
case Leaf(_) => 1
case Node(left, right) => size(left) + size(right)
}
/* no, again the last expression is the `+` operator
* and we create two recursion branches
*/
Let's Code: RecursiveFunctions
sbt> project fp101-exercises
sbt> test:testOnly exercise2.RecursiveFunctionsSpec
But what with all the other recursion types?
There is no tool Scala or the JVM can provide us here. We have to rely on a technique called Trampolining. We won't discuss that in this course.
represent collections
solve complex problems with divide & conquer
Composition: definition
Build complex programs out of simple ones.
Composition: math
\[\begin{aligned} f : b \rightarrow c \\ g : a \rightarrow b \\ \newline f . g : a \rightarrow c \end{aligned} \]
Composition: code
def compose[A, B, C](f: B => C)(g: A => B): A => C =
a => f(g(a))
def double(a: Int): Int = a * 2
def show(a: Int): String = a.toString
val complex = compose(show)(double)
complex(2) == "4"
Composition: Scala
// already built-in
(show _).compose(double)
// ^
// '- transforming method to a function value
// or work directly with function values
val show: Int => String = _.toString
val double: Int => Int = _ * 2
show.compose(double)
Composition: Scala
double.andThen(show) == show.compose(double)
Composition: data structures
val strs = Cons("Hello", Cons("world", Nil()))
def strToChars(str: String): List[Char] = ???
def upperCase(a: Char): Char = ???
How to compose `strToChars` and `upperCase`?
Composition: data structures
// we already know map (exercises)
def map[A, B](as: List[A])(f: A => B): List[B]
val chars = map(strs)(a => strToChars(a))
// we need to flatten this structure
chars: List[List[Char]]
Composition: data structures
// list[list[a]] -> list[a]
def flatten[A](as: List[List[A]]): List[A] = as match {
// empty case
case Nil() => Nil()
// recursive step
case Cons(a, tail) =>
append(a, flatten(tail))
}
Composition: data structures
val chars = flatten(map(strs)(a => strToChars(a)))
chars: List[Char]
Composition: data structures
// or we combine them
def flatMap[A, B](as: List[A])(f: A => List[B]): List[B] =
flatten(map(as)(f))
val chars = flatMap(strs)(a => strToChars(a))
chars: List[Char]
Composition: data structures
val complex: List[String] => List[Char] =
strs => map(flatMap(strs)(strToChars))(show)
val list = Cons("hello", Cons("world", Nil()))
complex(list) == Cons('H', Cons('E', ...))
Composition: for-comprehension
// make map and flatMap methods of List
sealed trait List[A] {
def map[B](f: A => B): List[B]
def flatMap[B](f: A => List[B]): List[B]
}
Composition: for-comprehension
// instead of
val complexFlat = List[String] => List[Char] = strs => {
strs.flatMap { str =>
strToChars(str).map { char =>
upperCase(char)
}
}
}
// Scala lets you use for-comprehension
val complexFor: List[String] => List[Char] = strs =>
for {
str <- strs
char <- strToChars(str)
} yield upperCase(char)
complexFor(list) == complexFlat(list)
Composition: for-comprehension
// comes in handy later on
for {
a <- f(in)
b <- g(a)
...
z <- h(???)
} yield doSomething(z)
f(in).flatMap{ a =>
g(a).flatMap { b =>
... {
soSomething(z)
}
}
}
Let's Code: Compositions
sbt> project fp101-exercises
sbt> test:testOnly exercise2.CompositionsSpec
solve complex problems with divide & conquer
Immutability
Algebraic Data Structures
Pure Functions
$f : a \rightarrow b$
Referential Transparency
Recursion
Composition