Object-Oriented and Functional Programming: exam prep

Exam: Thursday 22 October 2026, 12:15 to 14:30, Testvision. 27 questions, multiple choice and multiple response. Your answers below are saved in this browser only.

27
questions per exam, every year since 2019
5 min
per question (135 minutes total)
378
past questions below, all with answers and explanations
≥ 5.5
needed on the exam itself; it counts for 40% of your grade

The plan: Wednesday 11:15 to Thursday 10:45

Ordered by how often each week shows up. On an average past exam, questions touch week 4 about 9 times, week 6 about 7, week 3 about 6, week 2 about 6, week 1 about 4, and weeks 5 and 7 about 2 each (one question can touch two weeks). So weeks 3, 4 and 6 are where the 8 or 9 is won.

The loop for every block: read the lesson (from zero, with worked examples) → do the "Try it" boxes on paper first, then open the answer → click the past-question chips under each topic and answer them on the spot → whatever you got wrong, read the explanation and the "Trap" box again.

Wednesday 21 October starting now, about 10 hours of study

11:15-12:30
Week 1 and 2: Scala basics, mutability and purity. The box-and-arrow memory picture is the single most useful tool for the whole exam. Do the aliasing Try it boxes on paper, skim the rest.
12:30-13:45
Week 3: advanced functions. Closures, def / val / lazy val, currying, placeholder syntax. Count side effects carefully.
13:45-14:15
Lunch.
14:15-15:45
Week 4, part 1: polymorphism. Dynamic dispatch traces, overloading versus overriding, abstract and final. Use the static type / dynamic type columns on every trace.
15:45-17:15
Week 4, part 2: higher-order functions and folds. Desugar for-loops by hand, foldLeft versus foldRight bracket by bracket. Week 4 is about 9 questions per exam, so do not rush this one.
17:15-18:00
Week 5: pattern matching, Option, Either, tail recursion. Only about 2 questions per exam: read the lesson, do one Try it per topic.
18:00-18:45
Dinner.
18:45-20:15
Week 6, part 1: subtyping and lub. Draw the hierarchy, walk up from both types. This topic alone is about 4 questions per exam.
20:30-21:45
Week 6, part 2 and week 7: traits, linearization, variance, type bounds, then the language zoo table. Learn the table well: it is the cheapest points on the exam.
21:45-22:30
In Practice, 15 random questions from weeks 4 and 6. Write every mechanism you miss in one line on a sheet ("lub of Int and String is Any, not AnyVal"). Then stop and sleep.

Thursday 22 October morning, exam at 12:15

07:30-09:45
Mock exam: 2025 main exam, timed (135 min), in the Mock exam section. It is the most recent one and the closest to your exam's style. No lessons open, paper and pen next to you.
09:45-10:30
Review the mock. For every wrong answer, read the explanation and the Trap box of that topic. Then read every Recap card and your sheet of missed mechanisms.
10:45
Stop studying. Eat, get there early. Take your campus card or ID.

Behind schedule? Cut in this order: week 5 depth, then week 7 depth (keep the table), then the week 1 Snake framework part, then the 21:45 drill. Never cut weeks 3, 4 or 6, and do not study past midnight.

The other 12 past exams (2019 to 2023 and all resits) are in the practice bank, linked from every topic. Use them for drilling, not as mocks.

How to answer any question

The professor's own advice: the exam is built so that you cannot pass by memorising question types. Two questions can look the same and need different reasoning because one detail changed (a val became a var, a List became an Array, a method became final). For every question, ask yourself:

  1. Which mechanism is being tested? Aliasing? Dynamic dispatch? Evaluation order? lub? Variance? Name it before you compute anything.
  2. Apply that mechanism step by step on paper. Draw the memory boxes, the hierarchy, the state table, or the fold brackets. Do not do it in your head.

Paper tools (each one is taught in the lessons)

Question smells likeTool
Arrays, objects, var, "what does this print" after updatesBox-and-arrow memory picture (week 1, 2)
Closures, def / val / lazy val, prints inside functionsA timeline: write down every moment a side effect fires (week 3)
Classes calling overridden methods, superTwo columns: static type (decides overloads, what compiles) and dynamic type (decides overrides) (week 4)
for / yield, map, flatMap, foldsDesugar by hand, write out the brackets of the fold (week 4)
matchTry cases top to bottom, the first match wins, check the guard (week 5)
"What is the type of", lub, if / List of mixed valuesDraw the hierarchy, walk up from both types (week 6)
Traits mixed together, super.m()Linearization, deepest first, last trait first (week 6)
"Is F[A] a subtype of F[B]", function typesVariance sign rules (week 6)

Multiple response questions

Judge every option on its own as true or false, as if it were a separate question. Do not stop after you find one true option, and do not assume there must be two. About a third of all past questions are multiple response.

Time

135 minutes for 27 questions is 5 minutes each. If a trace gets past 8 minutes, mark it, move on, and come back. In Testvision, Control and + makes the code bigger.

Lessons

From zero. Every code example with an output was run with Scala 2.13. Under each topic, the chips are the real past exam questions on it: click one to answer it right there.

Week 1: Scala basics and the basics of programming

Week 1 gives you the vocabulary for every other week: expressions, val and var, scopes, classes, objects and references. The exam opens almost every year with two or three "what does this print" questions built only from these pieces: a map/filter pipeline, a program with globals and look-alike local names, an array of objects that share one box, and a Snake GameLogic class whose picture you must imagine. None of these is hard once you have a paper method, so this lesson gives you one for each.

Scala in one page: expressions, methods, ranges, printing

Scala is a statically typed language that runs on the Java virtual machine and mixes object oriented and functional programming. "Statically typed" means the compiler knows the type of every expression before the program runs, and refuses programs where types do not fit. A few facts explain most of the code you will read on the exam.

Almost everything is an expression. An expression is a piece of code that produces a value. In Scala, if is an expression (it gives a value, like the ? : operator in Java), and a block { ... } is an expression whose value is the value of its last line. A method body is usually just one expression.

val n = 7
val kind = if (n % 2 == 0) "even" else "odd"   // if gives a value
println(kind)
val total = { val a = 3; val b = 4; a * b }    // value of a block = last expression
println(total)
odd
12

Methods are defined with def name(param: Type): ResultType = body. The result type can often be left out (type inference). Unit is the result type of a method that returns nothing useful (like void in Java). n % m is the remainder, so n % m == 0 means "n is a multiple of m". Note that 0 % m == 0 is true: 0 is a multiple of every number.

Ranges. 1 to 5 includes both ends (1,2,3,4,5). 1 until 5 stops before the end (1,2,3,4). Mapping over a range gives a Vector, which is why some exam answers print as Vector(...).

println((1 to 5).toList)
println((1 until 5).toList)
println((1 to 3).map(_ * 2))
List(1, 2, 3).foreach(print)                 // no separators, no newline
println()
List(1, 2, 3).foreach(x => print(x + " "))   // a space after each element
List(1, 2, 3, 4, 5)
List(1, 2, 3, 4)
Vector(2, 4, 6)
123
1 2 3 

_ * 2 is short for x => x * 2, an anonymous function (Week 3 explains this placeholder notation fully). foreach(print) calls print on each element: everything comes out glued together.

return leaves the whole method, not just the loop. A for loop that contains a return in every iteration therefore only ever runs its first iteration. Compare these two methods: one small change, a different answer.

def firstBig(l: List[Int]): Int = {
  for (e <- l) {
    if (e > 10) return e      // returns only when the test holds
  }
  -1
}
def oops(l: List[Int]): Int = {
  for (e <- l) {
    return if (e > 10) e else 0   // returns in the very first iteration
  }
  -1
}
println(firstBig(List(3, 12, 40)))
println(oops(List(3, 12, 40)))
12
0
The rule if and blocks are expressions; a block's value is its last line. to includes the end, until excludes it. return exits the whole method immediately.
Trap Reading 0 until 15 as including 15, forgetting that 0 is a multiple of 3, 4 and 5, or expecting spaces between items printed with foreach(print). Each of these is a wrong option on a past exam.
On the exam Question 1 is usually a small program that prints a sequence. First write down the exact range (first and last element), then apply the steps of the pipeline in order (next topic), then glue the output exactly as the print statement does.
Try it: what does this print?
val r = { val a = 2; if (a > 1) a * 10 else 0 } + 1
println(r)
println((1 until 4).map(i => i * i))

The block: a = 2, the if is true, so its value is 20; that is the last line, so the block is 20. Plus 1 gives 21. 1 until 4 is 1,2,3; squares are 1,4,9, and mapping a range gives a Vector.

21
Vector(1, 4, 9)
Try it: what does this print?
def g(l: List[Int]): Int = {
  for (e <- l) if (e % 2 == 0) return e * 10
  99
}
println((g(List(1, 3, 4, 6)), g(List(1, 3)), g(List())))

First list: 1 and 3 are odd, 4 is even, so return 40 leaves the method (6 is never looked at). Second list: no even number, the loop ends and the last line gives 99. Empty list: the loop body never runs, so 99.

(40,99,99)

Reading pipelines and if-chains (the question 1 pattern)

A pipeline is a chain like range.map(f).filter(p).map(g). Each step makes a new collection from the previous one:

  • map(f): replace every element x by f(x). Same number of elements.
  • filter(p): keep only the elements for which p(x) is true. Order is kept.
  • filter(!isMultipleOf(_, 3)): the ! flips the test, so it keeps the non-multiples.

The steps run left to right, and each step sees the values produced by the step before it, not the original numbers. That is the whole trick of these questions: the exam swaps the order of two steps and the answer changes.

Paper method: the pipeline table. One row per starting element, one column per step. Write the value after each step; when a filter rejects a value, write "x" and stop that row. At the end, read the last column top to bottom.

def squash(n: Int): Int = if (n % 4 == 0) 1 else n
println((0 until 10).map(_ + 1).map(squash).filter(_ > 2).map(_ * 10))
println((0 until 10).map(squash).map(_ + 1).filter(_ > 2).map(_ * 10))
start+1squash> 2?*10
011x
122x
233keep30
341x
455keep50
566keep60
677keep70
781x
899keep90
91010keep100

In the second line squash runs before + 1, so it sees 0..9 instead of 1..10. Now 0, 4 and 8 become 1 (0 is a multiple of 4!), and then +1 makes them 2, which the filter drops. Same steps, different order, different output:

Vector(30, 50, 60, 70, 90, 100)
Vector(30, 40, 60, 70, 80, 100)

If-chains: the first true test wins. In if (A) .. else if (B) .. else if (C) .., Scala tests A, then B only if A was false, and so on. Later tests are never looked at once one matched. So the order of the tests matters when they overlap, as "multiple of 6" and "multiple of 2" do.

def isMultipleOf(n: Int, m: Int): Boolean = n % m == 0
def label(n: Int): String =
  if (isMultipleOf(n, 6)) "ab"
  else if (isMultipleOf(n, 2)) "a"
  else if (isMultipleOf(n, 3)) "b"
  else n.toString
def label2(n: Int): String =        // same tests, the 6-test moved last
  if (isMultipleOf(n, 2)) "a"
  else if (isMultipleOf(n, 3)) "b"
  else if (isMultipleOf(n, 6)) "ab"
  else n.toString
(1 to 7).map(label).foreach(print)
println()
(1 to 7).map(label2).foreach(print)
1aba5ab7
1aba5a7

In label2, 6 is caught by the "multiple of 2" test first, so the "ab" branch can never be reached.

The rule Apply the steps one at a time, in the written order, each to the output of the previous step. In an if-chain the first true condition wins.
Trap Filtering the original numbers instead of the already mapped ones, applying a function before the +1 that comes first, forgetting the last map(_ - 1), or counting a number twice in an if-chain. Every past "question 1" has exactly these wrong options.
On the exam Draw the pipeline table. Ten rows take two minutes and remove all doubt. Check the start and end of the range, mark every filter rejection, and only then glue the output (with or without spaces, as printed).
Try it: what does this print?
def tag(n: Int): String =
  if (isMultipleOf(n, 10)) "T" else if (isMultipleOf(n, 5)) "F" else n.toString
(0 until 12).filter(_ % 2 == 1).map(_ + 4).map(tag).foreach(print)

Odd numbers below 12: 1,3,5,7,9,11. Plus 4: 5,7,9,11,13,15. tag: 5 is a multiple of 5 but not of 10, so "F"; 7, 9, 11, 13 stay; 15 gives "F". Glued:

F791113F
Try it: same steps, two orders
def half(n: Int): Int = if (isMultipleOf(n, 3)) n / 3 else n
(1 to 9).map(half).filter(_ >= 3).map(_ * 2).foreach(x => print(x + " "))
println()
(1 to 9).filter(_ >= 3).map(half).map(_ * 2).foreach(x => print(x + " "))

Line 1: half of 1..9 is 1,2,1,4,5,2,7,8,3. Keep values of at least 3: 4,5,7,8,3. Double: 8 10 14 16 6.
Line 2: filter first keeps 3..9. half: 1,4,5,2,7,8,3. Nothing is filtered after that, so the small values survive. Double: 2 8 10 4 14 16 6.

8 10 14 16 6
2 8 10 4 14 16 6 
Try it: true or false?
println(List(3, 8, 1).map(_ + 2).filter(_ > 4).exists(_ % 5 == 0))
println(List(1, 2).map(_ + 2).filter(_ > 4).exists(_ % 5 == 0))

exists(p) is true when at least one element passes p. First: 5,10,3, filter keeps 5 and 10, and 5 is a multiple of 5: true. Second: 3,4, the filter keeps nothing, and exists on an empty list is false.

true
false

var vs val

A variable is a name with a value attached. Scala has two kinds:

  • val x = ...: the name is bound once. Writing x = ... later is a compile error.
  • var x = ...: the name can be re-bound with x = .... x += 3 is short for x = x + 3.
var b = 1
b = 2
b += 3
println(b)
val a = 1
a = 2          // does not compile
5
error: reassignment to val

Here is the part that matters for the exam. A val fixes which object the name points to, not what is inside that object. An Array is a mutable object (its slots can be changed), so a val holding an array still lets you change the slots:

val arr = Array(1, 2, 3)
arr(0) = 99           // fine: changes a slot inside the array object
println(arr.toList)
arr = Array(3)        // does not compile: re-binds the val
List(99, 2, 3)
error: reassignment to val

The opposite combination also exists: a var holding an immutable value. Then the name can move to a new value, but the old value never changes, so anyone who copied the old one keeps seeing it:

var v = Vector(1, 2)
val w = v            // w points to the same Vector(1, 2)
v = v :+ 3           // :+ builds a NEW vector; v now points to it
println((v, w))
(Vector(1, 2, 3),Vector(1, 2))

A subtle consequence: s += 3 on a var holding an immutable Set compiles, because Scala rewrites it to s = s + 3 (a new set). On a val holding an immutable set it does not compile, since there is neither a mutating += nor a variable to re-bind:

error: value += is not a member of scala.collection.immutable.Set[Int]
  Expression does not convert to assignment because receiver is not assignable.

Parameters of a method behave like vals: def reset(a: Array[Int]): Unit = a = Array(0) gives "reassignment to val".

The rule val vs var is about the name (can it be pointed somewhere else?). Mutable vs immutable is about the object (can its contents change?). These are independent; Week 2 makes a 2x2 table of them.
Trap "val m2 = m1, so m2 is a safe copy" is wrong when m1 is an array or another mutable object: both names point to one object, and a change through either name is seen through both.
On the exam When a question shows val next to an Array, ArrayBuffer or a class with var fields, ignore the word val for the contents. Ask only: which object does each name point to (see the aliasing topic below)?
Try it: what does this print?
var i = 0
for (i <- 0 until 3) {}
println(i)
for (j <- 0 until 3) i += j
println(i)

for (i <- ...) declares a new i that only lives inside the loop. It hides (shadows) the outer var i, which stays 0. The second loop uses a different name j and really changes the outer i: 0+0+1+2 = 3.

0
3
Try it: which lines compile?
var s1 = Set(1, 2);  s1 += 3                              // (a)
val s2 = Set(1, 2);  s2 += 3                              // (b)
val s3 = scala.collection.mutable.Set(1, 2);  s3 += 3     // (c)

(a) compiles: it becomes s1 = s1 + 3. (b) fails with the error shown above: immutable set, and the name cannot be re-bound. (c) compiles: a mutable set has its own += method that changes the set in place, so the val is never re-bound.

Scoping: which variable does this name mean?

The scope of a declaration is the part of the program where its name can be used. Scala uses lexical scoping: which variable a name refers to is decided by where the code is written, not by who happens to call it. The rule for finding it: start where the name is used and look outward, through the enclosing blocks, to the first declaration with that name.

A parameter (def p(a: Int)) and a local variable (var c = ... inside a method) are new variables that exist only during that call. If they have the same name as a global variable, they shadow it: inside that method the name means the local one, and the global one cannot be reached by that name. Without a declaration, a plain assignment like c = 5 or b += a changes the variable from the enclosing scope, usually the global one.

Paper method, two passes.

  1. Label every name. For each method, list its parameters and the names it declares with val/var. Every other name in it is global (or belongs to an enclosing method). Write a small subscript on each use: b<sub>G</sub> for global, a<sub>p</sub> for p's own.
  2. Run with a state table. One column per global, plus a column for the locals of the call that is running. Execute line by line. When a call returns, its locals disappear and its result goes into the assignment that called it. An assignment of the result happens after the call, so it overwrites whatever the call itself did to that variable.
var a = 2
var b = 10
var c = 0
def p(a: Int): Int = {      // a = p's parameter; b, c = global
  b += a
  c = a * 2
  a + 1
}
def q(b: Int, d: Int): Int = {   // b, d = parameters; a = global
  var c = b + d                  // NEW local c, hides global c
  a = p(c)
  c + b
}
def run(): Unit = {
  a = p(b)
  b = q(a, 1)
  println((a, b, c))
}
run()
stepaGbGcGlocals of the running call
start2100
call p(bG)2100ap = 10
b += a2200ap = 10
c = a * 222020returns 11
a = 11112020
call q(11, 1)112020bq = 11, d = 1
var c = b + d112020cq = 12
call p(12): b += 12, c = 24113224p returns 13
a = 13133224cq = 12, bq = 11
q returns cq + bq = 23; b = 23132324
(13,23,24)

Note the last row: p had raised the global b to 32, but the assignment b = q(...) runs after q returns and overwrites it with 23.

One small change. Delete the word var in q, so the line becomes c = b + d. Now that line writes the global c (12), p overwrites it with 24, and q returns c + b = global c 24 + bq 11 = 35:

(13,35,24)

Lexical, not dynamic. A method only sees its own names and the names around its definition. It never sees the locals of whoever called it:

var k = 1
def h(x: Int): Int = k + x                  // k is ALWAYS the global k
def g(x: Int): Int = { var k = 100; h(x) }  // this local k is invisible to h
println(g(5))
6

Nested methods are the exception that proves the rule: a method defined inside another method sees the outer method's locals, because they are lexically around it.

var x = 0
def outer(y: Int): Int = {
  var x = y                                        // outer's local x
  def inner(z: Int): Int = { x -= z; z + 1 }       // this x is outer's x
  val r = inner(3)
  x + r
}
println(outer(10))
println(x)
11
0

inner(3) makes outer's x 7 and returns 4; outer returns 7 + 4 = 11. The global x is never touched.

The rule A name means the nearest enclosing declaration in the source text. var x = ... or a parameter x creates a new variable; a bare x = ... changes an existing one. A method never sees its caller's locals.
Trap (1) var y = yi + x inside a method does not change the global y. (2) A parameter called a is not the global a, even when the argument passed in was the global a. (3) c += ... inside a method does change the global c when there is no local c. (4) x = f(...) overwrites whatever f did to x. (5) for (i <- ...) declares a new i.
On the exam Every year has one "globals with confusing names" question (answers like 42, 12, 5, 22). Do the labelling pass first, before computing anything; most wrong options come from one mislabelled name. Then keep the state table strictly line by line.
Try it: what does this print?
var n = 1
var m = 0
def f(m: Int): Int = {
  n += m
  m * 2
}
def g(n: Int): Int = {
  val r = f(n + 1)
  m = r + n
  r
}
println((g(n), n, m))

Labels: in f, m is the parameter and n is global. In g, n is the parameter and m is global.
g(1): ng = 1. Call f(2): mf = 2, global n becomes 1 + 2 = 3, f returns 4. r = 4. m = r + n: global m = 4 + ng (1) = 5. g returns 4. The tuple is built left to right, so n and m are read after the call.

(4,3,5)
Try it: what does this print?
var total = 0
def add(total: Int, x: Int): Int = total + x
def addAllWrong(): Unit = for (i <- 1 to 3) add(total, i)
def addAll(): Unit = for (i <- 1 to 3) total = add(total, i)
addAllWrong(); println(total)
addAll(); println(total)

add only reads its parameter called total; it never writes the global. addAllWrong throws the results away, so the global stays 0. addAll stores each result: 0+1, +2, +3 = 6.

0
6
Try it: what does this print?
var v = 3
def a(): Int = { v = v * 2; v }
def b(v: Int): Int = { val w = a(); v + w }
println(b(v))
println(v)

b(3): parameter vb = 3 (a copy of the value). a() doubles the global v to 6 and returns 6. b returns vb + w = 3 + 6 = 9. The global is now 6.

9
6

Classes and constructors

A class is a blueprint; an object (or instance) is one thing built from it with new. Each new builds a fresh, separate object with its own fields. A field is a variable that lives inside an object.

In Scala the class header is the main (primary) constructor: class Counter(start: Int) takes one argument when you write new Counter(5). All statements in the class body run once, top to bottom, when the object is built.

class Counter(start: Int) {
  var count: Int = start                     // a field, set during construction
  println("made a Counter at " + start)      // body runs at construction
  def inc(): Unit = count += 1
}
val c1 = new Counter(5)
c1.inc(); c1.inc()
val c2 = new Counter(5)
println(c1.count + " " + c2.count)
made a Counter at 5
made a Counter at 5
7 5

Three kinds of constructor parameter.

headeroutside code can read p.x?can write p.x = ...?
class P(x: Int)no ("value x is not a member of P")no
class P(val x: Int)yesno ("reassignment to val")
class P(var x: Int)yesyes: the object is mutable

Auxiliary constructors are extra constructors written def this(...). Their first statement must call another constructor of the same class, so in practice they supply default values:

class Lamp(var on: Boolean) {
  def this() = this(false)        // new Lamp() means new Lamp(false)
  def toggle(): Unit = on = !on
}
val l1 = new Lamp()
val l2 = new Lamp(true)
l1.toggle()
println((l1.on, l2.on))
(true,true)

Identity. For an ordinary class, == asks "is this the very same object?". Two objects built separately with equal fields are not equal; two names pointing to one object are:

class PlainPoint(val x: Int, val y: Int)
val pa = new PlainPoint(1, 2)
val pb = new PlainPoint(1, 2)
println(pa == pb)        // two different objects
val pc = pa
println(pa == pc)        // same object
false
true

(Case classes, Week 2, change this: they compare fields.)

Nested classes. A class written inside another class (Week 3 covers these) can use the fields of the outer class. Every inner object remembers which outer object it was made from: new t1.Player(2) belongs to the object t1. Draw this as a hidden arrow from the inner object to its outer object. All inner objects of the same outer object change the same outer fields.

class Team(var score: Int) {
  class Player(var bonus: Int) {
    def scoreGoal(): Unit = { score += bonus; bonus += 1 }   // score = the outer Team's field
  }
}
val t1 = new Team(0)
val t2 = t1                    // same Team object, second name
val p = new t1.Player(2)
val q = new t2.Player(10)      // also belongs to that one Team
p.scoreGoal(); p.scoreGoal(); q.scoreGoal()
println(t1.score)
val t3 = new Team(0)
val r = new t3.Player(1)
r.scoreGoal()
println((t1.score, t3.score, p.bonus, q.bonus, r.bonus))
15
(15,1,4,11,2)

p scores 2 then 3 (its bonus grew), q scores 10, all into the one Team: 15. t3 is a different Team, so r only changes t3.

The rule The class body is the constructor and runs once per new. Each new makes a separate object with its own fields. var parameters make public mutable fields, val read-only ones, plain parameters private ones.
Trap Thinking two inner objects have their own copy of the outer field, or that val a2 = a creates a second outer object. Also: reassigning a variable (b = new a.B(4)) does not undo what the old object already did.
On the exam For questions with several classes, draw every new as a box with its fields, and every inner object with an arrow to its outer box. Then execute the method calls and update the boxes.
Try it: what does this print?
class Lamp(var on: Boolean) {
  def this() = this(false)
  def toggle(): Unit = on = !on
}
val a = new Lamp()
val b = a
val c = new Lamp()
b.toggle(); c.toggle(); c.toggle()
println((a.on, b.on, c.on))

a and b name the same lamp (built with false), toggled once: true. c is its own lamp toggled twice: false. This prints (true,true,false). The same mechanism appears in the identity and nested-class runs above.

Objects (singletons)

The keyword object defines a class and its one and only instance at the same time. This is called a singleton. You never write new for it; you use its name directly. Its fields are effectively global variables, and its methods are like static methods in Java. A program's entry point is a main method in an object: object Foo { def main(args: Array[String]): Unit = ... }.

object Settings {
  val Width = 10
  var clicks = 0
  def click(): Int = { clicks += 1; clicks }
}
Settings.click(); Settings.click()
println(Settings.clicks)
val s1 = Settings
val s2 = Settings
s1.click()
println(s2.clicks)         // s1 and s2 are the same single object
2
3

An object is created the first time it is used, not when the program starts. Its body then runs once:

object Logger {
  println("Logger created")
  def hi(): Unit = println("hi")
}
println("start")
Logger.hi()
Logger.hi()
start
Logger created
hi
hi
The rule object X = exactly one instance, named X, created on first use. Its vars are shared global state.
Trap The exam's scoping questions often put the "globals" inside object Scope { var a ...; def main ... }. These are fields of the single object and behave exactly like globals.
On the exam Treat every var in an object as a global column in your state table.
Try it: in which order do the lines appear?

Take the Logger example and imagine the first line of the program was Logger.hi() before println("start"). Then the object is first used at that line: you get Logger created, hi, start, hi. The body still runs only once.

Companion objects

A companion object is an object with the same name as a class, written in the same file. The two may read each other's private members. The companion is the place for things that belong to the class as a whole rather than to one instance: constants, and factory methods that build instances.

A method named apply is special: Rational(1, 2) is short for Rational.apply(1, 2). That is how you can create objects without new.

class Rational(val n: Int, val d: Int) {
  def +(r: Rational): Rational = Rational(n * r.d + r.n * d, d * r.d)
  override def toString: String = n + "/" + d
}
object Rational {
  def apply(n: Int, d: Int): Rational = {      // factory: always simplifies
    val g = gcd(n.abs, d.abs)
    new Rational(n / g, d / g)
  }
  def apply(n: Int): Rational = apply(n, 1)
  private def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)
  val Zero: Rational = new Rational(0, 1)
}
println(Rational(1, 2) + Rational(1, 3))
println(Rational(2, 4))
println(Rational.Zero)
println(new Rational(2, 4))      // new skips the factory
5/6
1/2
0/1
2/4

The Snake framework uses a companion for constants: GameLogic.FramesPerSecond and GameLogic.DefaultGridDims live in object GameLogic. A companion can also call a private constructor, so it can control how objects are made:

class Account private (val owner: String, private var balance: Int) {
  def deposit(x: Int): Unit = balance += x
}
object Account {
  def open(owner: String): Account = new Account(owner, 0)   // allowed: companion
  def balanceOf(a: Account): Int = a.balance                 // allowed: companion
}
val acc = Account.open("Sam")
acc.deposit(20)
println(Account.balanceOf(acc))
20
The rule Same name + same file = companions; they share private access. X(args) without new calls X.apply(args). Case classes get such an apply automatically.
Trap Rational(2, 4) and new Rational(2, 4) are not the same thing here: only the first goes through the factory.
On the exam Companions are rarely asked directly. You need them to read code: Point(2, 3) without new means a companion apply (usually from a case class), and Name.constant is a companion constant.
Try it: what does this print?
println(Rational(3, 6) + Rational.Zero)
println(Rational(4))

Rational(3, 6) is simplified by the factory to 1/2. Adding 0/1 gives Rational(1*1 + 0*2, 2*1) = Rational(1, 2) = 1/2. Rational(4) uses the one-argument apply: 4/1. So: 1/2 and 4/1.

Named and default arguments

A parameter can have a default value: if the caller leaves it out, the default is used. A caller can also name an argument (height = 1), which lets them skip earlier parameters or give arguments in any order.

def box(width: Int = 3, height: Int = 2, fill: Char = '#'): String =
  ((fill.toString * width) + "\n") * height
print(box())
print(box(height = 1))
print(box(5, fill = '.'))
print(box(fill = '*', width = 2))
###
###
###
.....
.....
**
**

Positional arguments fill parameters from the left. Once you have named an argument out of its position, positional arguments may not follow: box(height = 1, 4) gives "positional after named argument".

def greet(name: String, greeting: String = "Hello", end: String = "!"): String =
  greeting + ", " + name + end
println(greet("Ann"))
println(greet("Ann", end = "?"))
println(greet(greeting = "Hi", name = "Bob"))
println(greet("Cy", "Hey"))
Hello, Ann!
Hello, Ann?
Hi, Bob!
Hey, Cy!

You met this in Snake: case class SnakeBody(distanceToHead: Float = 0f), which is why both SnakeBody() and SnakeBody(0.5f) appear in exam code. The copy method of case classes (Week 2) is built on named and default arguments: p.copy(y = 5).

The rule Missing argument: use the default. Named argument: goes to that parameter wherever it is written. Unnamed arguments fill from the left.
Trap greet("Cy", "Hey") puts "Hey" into the second parameter (greeting), not into end.
On the exam Mostly a reading skill: when you see SnakeBody() or copy(x = ...), fill in the missing arguments with their defaults before you reason further.
Try it: what does print(box(2, 1, '-') + box(height = 1, fill = 'o')) print?

The first call is positional: width 2, height 1, fill '-': -- and a newline. The second: width default 3, height 1, fill 'o': ooo and a newline. Output: -- on one line and ooo on the next.

Tuples

A tuple groups a fixed number of values, possibly of different types, without defining a class: (3, "three", true) has type (Int, String, Boolean). You read the parts with ._1, ._2, ... (counting from 1), or unpack them with a pattern. Tuples are immutable.

val t = (3, "three", true)
println(t._1 + 1)
println(t)
val (num, word, _) = t                 // unpack; _ ignores a part
println(word * 2)
def minMax(l: List[Int]): (Int, Int) = (l.min, l.max)   // return two results
val (lo, hi) = minMax(List(4, 1, 9))
println(hi - lo)
println((1, 2) == (1, 2))              // tuples compare their parts
4
(3,three,true)
threethree
8
true

t._1 = 5 gives "reassignment to val". Tuples show up in three exam situations:

  • Printing: println((a, b)) prints (x,y) with no space after the comma.
  • Pairs in loops and maps: for ((l, r) <- pairs) unpacks each pair; a Map is a collection of key/value pairs, and (k, v) and k -> v are the same pair.
  • Types: the type of an if that returns tuples is computed per position (the Week 6 lesson on lub covers this). And a tuple can be the accumulator of a fold, so one pass computes two results.
val pairs = for (x <- List(1, 2); y <- List("a", "b")) yield (x, y)
println(pairs)
val joined = for ((k, s) <- pairs) yield s + k
println(joined)
println(List(1, 2, 3).foldLeft((0, 1))((acc, e) => (acc._1 + e, acc._2 * e)))
List((1,a), (1,b), (2,a), (2,b))
List(a1, b1, a2, b2)
(6,6)
The rule A tuple is an immutable fixed-size group; ._1 is the first part. Its type is the tuple of the part types.
Trap Treating a tuple like a List (one element type for all parts). (new Lion, new Cat) has type (Lion, Cat), and each position can be widened on its own.
On the exam Tuples are mostly glue: in output formatting, in map keys like (Truthy, Truthy), and in the type questions of Week 6.
Try it: what does this print?
println((1, 2).swap)
println((("x", 1), ("y", 2))._2._1)

swap exchanges the two parts: (2,1). In the nested tuple, ._2 is ("y", 2) and its ._1 is y.

(2,1)
y
Try it: what does the fold compute?

In List(1, 2, 3).foldLeft((0, 1))((acc, e) => (acc._1 + e, acc._2 * e)) the accumulator starts as (0,1). After 1: (1,1). After 2: (3,2). After 3: (6,6). So the first part is the sum and the second the product, both 6 here (output shown above).

Aliasing: references and the box-and-arrow picture

This is the most important topic of the block. The professor's slide says it in four words: everything is a reference.

An object lives somewhere in memory. Draw it as a box with its fields inside. A variable, a field, an array slot or a parameter does not contain the object; it contains a reference to it. Draw a reference as an arrow from the slot to the box. When two arrows point to the same box, the two names are aliases: different names for one object. A change made through one name is visible through the other, because there is only one box.

For immutable values like Int, Boolean and String the arrow can never be observed (nobody can change the box), so you may simply write the value in the slot.

The four rules of the picture.

  1. Only new C(...) (or a case class call like Point(1,2)) draws a new box. Nothing else creates objects.
  2. Assignment copies an arrow, never a box. x = y, arr(i) = y, passing y as an argument, putting it in a list, returning it: the target gets an arrow to the box y points to.
  3. Re-pointing changes one slot. arr(1) = new Cell(5) or b = ... moves only that one arrow. Other arrows to the old box stay where they are.
  4. Mutating changes a box. x.v += 1, arr(i).v = 3, buf.addOne(5): follow the arrow and change the box. Every name with an arrow to that box sees the change.

So for each line of code, ask: does it re-point an arrow (left side is a variable or slot), or change a box (left side has a dot: something.field, or calls a mutating method)?

class Cell(var v: Int)
val a = new Array[Cell](3)
val c = new Cell(0)
for (i <- a.indices) a(i) = c      // three arrows to ONE box
a(1) = new Cell(7)                 // re-point slot 1 only
a(0).v += 5                        // change the shared box
println(a.map(_.v).toList)
List(5, 7, 5)
ac 012 Cell v = 5Cell v = 7

Slots 0 and 2 (and c) still point to the one shared box, so the += 5 shows up twice. Slot 1 was re-pointed before the change, so it is not affected.

Copy the arrow vs copy the value. To get an independent copy you must build new boxes yourself:

val snap = new Array[Cell](3)
for (i <- snap.indices) snap(i) = new Cell(a(i).v)   // new boxes with the current values
a(2).v = 100                                          // the shared box again
println(a.map(_.v).toList)
println(snap.map(_.v).toList)
List(100, 7, 100)
List(5, 7, 5)

Chains. arr(i) = arr(i - 1) in a loop makes every slot point to the first box, so every += lands in that one box:

val chain = new Array[Cell](4)
chain(0) = new Cell(0)
for (i <- 1 until chain.length) { chain(i) = chain(i - 1); chain(i).v += 2 }
println(chain.map(_.v).toList)
List(6, 6, 6, 6)

Three increments of 2 into one box: 6. And because all four slots show that box, a sum over the array counts it four times (24).

Objects that hold references. A field can hold an arrow too. Re-pointing the field inside one object does not move any other arrow:

class Wallet(var euros: Int)
class Person(var wallet: Wallet) {
  def spend(x: Int): Unit = wallet.euros -= x                          // changes the box
  def newWallet(): Unit = wallet = new Wallet(wallet.euros + 1)        // re-points own field
}
val w = new Wallet(10)
val p = new Person(w)       // p.wallet and w: same box
p.spend(3)
println(w.euros)
p.newWallet()               // p.wallet now points to a NEW box (8); w still to the old (7)
p.spend(5)
println(w.euros)
println(p.wallet.euros)
7
7
3

Passing to a method copies the arrow. The method can change the box you passed, and the caller sees it:

def shout(words: Array[String]): Unit = { words(0) = words(0).toUpperCase }
val w1 = Array("hi", "there")
val w2 = w1
shout(w2)
println(w1.toList)
println(w2.toList)
List(HI, there)
List(HI, there)

Arrays also compare by identity: Array(1, 2) == Array(1, 2) is false (two boxes), while List(1, 2) == List(1, 2) is true (lists compare contents).

The rule Only new (or a case class constructor) makes a box. Assignments, parameters and collections copy arrows. "Name = ..." re-points one arrow; "name.field = ..." changes a box that every alias sees.
Trap The exam flips the answer with one small change. Mutable class class Point(var x: Int, var y: Int) with a(i).move(...): all slots sharing one point accumulate every move. Immutable case class with a(i) = a(i) + Point(...): each slot is re-pointed to its own new point, and the shared original never changes. Same loop shape, completely different sums. See Week 2, immutable data, for this exact pair run side by side.
On the exam Every year has an "array of objects" question. Steps: (1) draw the array as a row of slots; (2) draw a box for each new and the arrows exactly as the loop creates them (watch i % 2 == 0 conditions, and val b = new B() created once outside the loop); (3) execute the changes, deciding for each line: re-point or mutate; (4) read the final values by following arrows, counting a shared box once per slot that points to it.
Try it: what does this print?
class Counter(var n: Int)
val k = new Counter(1)
val cs = Array(k, new Counter(1), k)
for (cc <- cs) cc.n *= 2
println(cs.map(_.n).toList)

Slots 0 and 2 point to k; slot 1 to its own box. The loop visits k twice: 1, 2, 4. The middle box is doubled once: 2.

List(4, 2, 4)
Try it: what does this print?
val ds = new Array[Counter](4)
for (i <- ds.indices) ds(i) = if (i % 2 == 0) new Counter(i) else ds(i - 1)
for (d <- ds) d.n += 1
println(ds.map(_.n).toList)

Slot 0: new box (0). Slot 1: arrow to the same box as slot 0. Slot 2: new box (2). Slot 3: same box as slot 2. Two boxes, each visited twice by the loop: 0 becomes 2, 2 becomes 4.

List(2, 2, 4, 4)
Try it: what does this print?
var u = new Counter(5)
val uu = u
u.n += 1          // mutate the box (both see 6)
u = new Counter(0)
u.n += 1          // mutate the NEW box
println((u.n, uu.n))

After line 3 the one box holds 6. Line 4 re-points only u to a new box; uu still points to the 6. The last increment goes into the new box.

(1,6)

Reverse Polish notation and the calculator

Normal arithmetic is written infix: the operator goes between its operands, (3 + 4) * 2, and you need brackets and precedence rules. In reverse Polish notation (RPN, also called postfix) the operator comes after its two operands: 3 4 + 2 *. No brackets are ever needed.

Evaluation with a stack. A stack is a pile: you can only add (push) on top and take (pop) from the top. Read the tokens left to right:

  • a number: push it;
  • an operator: pop the top (that is the right operand), pop the next (the left operand), push left op right.

At the end the stack holds one number, the answer. The order of the pops matters for - and /: 10 4 - is 10 - 4 = 6.

def applyOp(op: String, l: Int, r: Int): Int =
  if (op == "+") l + r
  else if (op == "-") l - r
  else if (op == "*") l * r
  else l / r
def isOp(t: String): Boolean = t == "+" || t == "-" || t == "*" || t == "/"
def evalRPN(input: String): Int = {
  var stack: List[Int] = List()            // head of the list = top of the stack
  for (token <- input.split(" ")) {
    if (isOp(token)) {
      val r = stack.head
      val l = stack.tail.head
      stack = applyOp(token, l, r) :: stack.tail.tail
    } else stack = token.toInt :: stack
    println(token + "  stack: " + stack)
  }
  stack.head
}
println(evalRPN("10 2 3 * -"))
10  stack: List(10)
2  stack: List(2, 10)
3  stack: List(3, 2, 10)
*  stack: List(6, 10)
-  stack: List(4)
4

The list is printed with the top of the stack on the left. * pops 3 and 2 and pushes 6; - pops 6 (right) and 10 (left) and pushes 10 - 6 = 4. The infix version is 10 - 2 * 3. Move one operator and the meaning changes: 10 2 - 3 * is (10 - 2) * 3 = 24.

Note how this implementation uses an immutable List as the stack and a var that is re-pointed to a new list at every token: a typical Week 2 style (immutable data, the variable moves).

The rule Numbers are pushed; an operator pops right then left and pushes left op right. To convert infix to RPN: write the operands in the same order and put each operator right after its two operands.
Trap Swapping the operands of - or / (the first pop is the right operand), or reading the printed list with the top on the wrong side.
On the exam RPN appeared as a lecture example and assignment, not as a recent exam question. If it shows up, draw the stack after every token, exactly like the trace above.
Try it: evaluate 5 1 2 + 4 * + 3 -

Push 5, 1, 2. +: 1+2 = 3, stack (top first) 3, 5. Push 4. *: 3*4 = 12, stack 12, 5. +: 5+12 = 17. Push 3. -: 17-3 = 14.

14
Try it: write (2 + 3) * (7 - 4) in RPN and evaluate it

2 3 + 7 4 - *. Stack: 2; 3,2; + gives 5; 7,5; 4,7,5; - gives 7-4 = 3, stack 3,5; * gives 15.

15
Try it: evaluate 8 2 / 3 -

/ pops 2 (right) and 8 (left): 8/2 = 4. Then 4 - 3 = 1. If you swapped the operands you would get 2/8 = 0 in integer division, a typical wrong option.

1

The Snake framework: "what does this GameLogic display?"

In assignment 2 you wrote only the class GameLogic. The framework (SnakeGame, which you did not change) owns the window and calls your methods. To answer the exam questions you need to know exactly when it calls what. Paraphrased from the real framework code:

// framework, at the start (once):
gameLogic = new GameLogic(random, gridDims)       // your constructor and class body run once

// framework, many times per second (draw):
if (it is time for the next frame) {              // FramesPerSecond = 5 in the real game
  gameLogic.step()                                // ONE step: update your state
}
for (p <- gridDims.allPointsInside)               // every cell, row by row
  paint(p, gameLogic.getCellType(p))              // SnakeBody = green, Empty = white

// framework, when a key is pressed:
gameLogic.changeDir(direction)                    // arrow keys

So the picture you see is always a function of the current state: step() changes the state once per frame, and getCellType(p) is asked, for every cell, "what is at position p right now?". It should only read the state; the framework calls it once per cell per frame (30 calls per frame on a 6 by 5 grid, as a counting run confirmed).

Coordinates. Point(x, y): x is the column, 0 at the left, growing to the right. y is the row, 0 at the top, growing downwards. So "y increases" means "moves down". gridDims.width is the number of columns, gridDims.height the number of rows; valid x are 0 to width - 1.

Cell types. SnakeBody(f) is a green cell; the number f (0 to 1) only chooses the shade of green (the distance to the head). It never moves anything. Empty() is a white cell. Older exams (2019, 2020) use the old API getCellType(x: Int, y: Int) or getGridType(x, y) and SnakeBody(1.0f): same idea.

Paper method: simulate a few frames.

  1. State. List the vars and their start values (the class body runs once).
  2. Time. Make a frame table: frame 0 = start state, frame k = state after k calls of step(). Fill in 4 to 6 frames, until you see a pattern (it repeats, bounces, or toggles). If there is no step or it changes nothing, the picture is static.
  3. Space. For one frame, plug the state values into getCellType and read the condition as a shape, using the dictionary below. Draw it on a tiny 5 by 5 grid if in doubt.
  4. Movie. Compare consecutive frames: what moves, blinks, grows or alternates? Match that with the options.

Worked example: a row that moves down and wraps around.

class RowLogic(val gridDims: Dimensions) {
  var r = 0
  def step(): Unit = r = (r + 1) % gridDims.height
  def getCellType(p: Point): CellType = if (p.y == r) SnakeBody() else Empty()
}

State: r. Frames: r = 0, 1, 2, 3, 4, then % height (height 5) makes it 0 again. Shape: p.y == r is true for every x in row r: one full row. Movie: a row moving down, jumping back to the top after the bottom. A small simulator that does exactly what the framework does (step, then ask every cell) prints these frames side by side (# = SnakeBody, . = Empty, grid 6 by 5):

######   ......   ......   ......   ......   ######
......   ######   ......   ......   ......   ......
......   ......   ######   ......   ......   ......
......   ......   ......   ######   ......   ......
......   ......   ......   ......   ######   ......

Small change 1: if (p.y == r || p.x == r). || means "either one": row r plus column r, a plus shape whose centre (r, r) moves diagonally.

######   .#....   ..#...   ...#..
#.....   ######   ..#...   ...#..
#.....   .#....   ######   ...#..
#.....   .#....   ..#...   ######
#.....   .#....   ..#...   ...#..

Small change 2: if (p.y == r && p.x == r). && means "both": only the crossing point, a single cell moving diagonally.

#.....   ......   ......   ......
......   .#....   ......   ......
......   ......   ..#...   ......
......   ......   ......   ...#..
......   ......   ......   ......

The shape dictionary (each checked on a 9 by 7 grid; c, s, r, b stand for state values):

condition in getCellTypeshape
p.x == cone column (vertical line)
p.y == cone row (horizontal line)
c <= p.x && p.x < c + wa wide column, w columns wide
p.x == p.ydiagonal from the top-left corner going down-right
p.x + p.y == width - 1 (same as width - 1 - p.x == p.y)the other diagonal, from top-right going down-left
p.x % 2 == bevery other column
p.y % 2 == bevery other row
(p.x + p.y) % 2 == bcheckerboard
p.x % 2 == 0 || p.y % 2 == 0every other row and every other column filled (a grid of lines)
p.x % 2 == 0 && p.y % 2 == 0isolated dots
dx == s && dy <= s || dy == s && dx <= s (dx, dy = distance to the centre per axis)outline of a square
dx <= s && dy <= sfilled square
dx + dy == soutline of a diamond
dy < ra horizontal band of rows around the centre
A || B / A && B / flag && Aboth shapes / only their overlap / shape A only in frames where flag is true

Some of these as the simulator draws them:

x % 2 == 0 || y % 2 == 0     outline (s = 2)     band dy < 2
#########                    .........           .........
#.#.#.#.#                    ..#####..           .........
#########                    ..#...#..           #########
#.#.#.#.#                    ..#...#..           #########
#########                    ..#...#..           #########
#.#.#.#.#                    ..#####..           .........
#########                    .........           .........

The time dictionary (how step changes the state):

step()effect over frames
b = 1 - b or flag = !flagtoggles 0,1,0,1: two pictures alternate (blinking, or alternating stripes/checkerboard)
r = (r + 1) % nmoves one cell per frame, wraps around to 0
x += dir, and dir = -dir at the edgesmoves to one side, then back (bounces)
r += dr, dr flips at a max and at 0grows and shrinks
state only changed in changeDirchanges only when a key is pressed

Old API scoping trap. With getCellType(x: Int, y: Int) the parameter x shadows the field x. Inside that method, x is the cell's column and this.x is the field. if (x == this.x) is a column at the field's position; if (x == x) would compare the parameter with itself and fill the whole screen.

The rule One step() per frame, then getCellType for every cell. The picture of a frame depends only on the state. y = 0 is the top row.
Trap Reading p.x == c as a single cell (it is a whole column: y is free), reading || as &&, treating the SnakeBody(...) number as a position, seeing all frames at once instead of one after the other, or forgetting the reset in step (wrap, bounce, or shouldMove = false).
On the exam One question per year since 2019 (2025 resit: a blinking cross). State, time, space, movie: about two minutes. Watch for variables that getCellType does not use at all, and for options describing movement in a direction the state cannot produce.
Try it: what does this display?
class Stripes(val gridDims: Dimensions) {
  var b = 0
  def step(): Unit = b = 1 - b
  def getCellType(p: Point): CellType =
    if (p.y % 2 == b) SnakeBody() else Empty()
}

State b toggles 0,1,0,... The condition uses only p.y: rows with the same parity as b. Frame 0: rows 0,2,4. Frame 1: rows 1,3. Alternating rows, blinking back and forth.

######   ......   ######
......   ######   ......
######   ......   ######
......   ######   ......
######   ......   ######
Try it: what does this display?
class Blink(val gridDims: Dimensions) {
  var on = true
  def step(): Unit = on = !on
  def getCellType(p: Point): CellType =
    if (on && p.x + p.y == gridDims.width - 1) SnakeBody() else Empty()
}

p.x + p.y == width - 1 is the diagonal from the top-right corner going down-left. on && shows it only in every other frame: a blinking diagonal.

.....#   ......   .....#
....#.   ......   ....#.
...#..   ......   ...#..
..#...   ......   ..#...
.#....   ......   .#....
Try it: what does this display?
class Bounce(val gridDims: Dimensions) {
  var x = 0
  var dir = 1
  def step(): Unit = {
    x = x + dir
    if (x == 0 || x == gridDims.width - 2) dir = -dir
  }
  def getCellType(p: Point): CellType =
    if (x <= p.x && p.x < x + 2) SnakeBody() else Empty()
}

Frame table (width 6): x = 0, 1, 2, 3, 4 (now x == width - 2, so dir becomes -1), 3, 2, ... The shape is columns x and x+1: a column two cells wide. Movie: a wide column moving right until it touches the right edge, then moving left, and so on.

##....   .##...   ..##..   ...##.   ....##   ...##.   ..##..

(Only the first row of each frame is shown; all rows are the same.)

Try it: what does this display?
class KeyLogic(val gridDims: Dimensions) {
  var pending = false
  var col = 0
  def changeDir(d: Direction): Unit = pending = true
  def step(): Unit = {
    if (pending) col = (col + 1) % gridDims.width
    pending = false
  }
  def getCellType(p: Point): CellType = if (p.x == col) SnakeBody() else Empty()
}

Without key presses, step changes nothing: a column stays put. A key press sets pending; the next step moves the column one to the right and clears pending. So: a column that moves one step right per key press (not continuously). Simulated with one key press before the second step:

#.....   #.....   .#....   .#....   .#....

Recap: Week 1 in one screen

conceptremember
expressionsif and blocks give values; a block's value is its last line; return leaves the whole method
rangesa to b includes b, a until b excludes b; 0 is a multiple of everything
pipelinessteps left to right, each on the previous output; draw a row per element, a column per step
if-chainfirst true test wins
val / varabout the name (can it be re-pointed?), not about the object's contents
scopinglexical: nearest declaration in the source; var x = and parameters make new variables; a bare x = changes the existing one; callers' locals are invisible
classesbody = constructor, runs per new; var/val/plain parameters; def this() calls another constructor; == on plain classes is identity
nested classesinner object has an arrow to its outer object; inner objects of one outer share its fields
objectone instance, created on first use; its vars are globals
companionsame name, same file, shared private access; X(..) = X.apply(..)
named/defaultmissing argument uses the default; named arguments go by name; no positional after an out-of-place named one
tuplesimmutable fixed-size groups; ._1 first; printed (a,b)
aliasingonly new makes a box; assignment copies arrows; "x = ..." re-points, "x.f = ..." mutates a shared box
RPNpush numbers; an operator pops right, then left, pushes left op right
Snakeone step() per frame, then getCellType(p) for every cell; y = 0 is the top; state, time, space, movie

Week 2: Mutability and purity, readable code

Week 2 is about change. "Mutate" simply means "change", and the professor's point is that code becomes easier to reason about when data is not changed but replaced by a new version, and when functions are pure: same input, same output, nothing hidden. The exam tests this with near-identical programs where one word (var, case, Array vs Vector, updated vs update) flips the output, and with "which of these are pure / equivalent" questions.

Two kinds of change

There are two completely separate ways something can "change" in a program, and most exam mistakes come from mixing them up:

  1. The variable changes: a var is re-pointed to a different value (v = v :+ 3). Only that one name is affected.
  2. The object changes: the contents of a mutable object are modified (arr(0) = 9, buf += 1, p.x += 1). Every name that points to that object sees it.

Immutable data is data that can only be created, never changed: List, Vector, the default Map and Set, String, Int, tuples, case classes with val fields. Mutable data can be changed in place: Array, ArrayBuffer, ListBuffer, mutable.Map/Set, classes with var fields. That gives four combinations:

immutable objectmutable object
valval a = Vector(1, 2): nothing can ever changeval c = Array(1, 2): contents can change, the name always points to this array
varvar b = Vector(1, 2): only the name can move to a new vectorvar d = Array(1, 2): both can change
var b1 = Vector(1, 2)
val bOld = b1
b1 = b1 :+ 3              // kind 1: re-point b1
println((b1, bOld))
val c1 = Array(1, 2)
val cAlias = c1
c1(0) = 9                 // kind 2: change the object
println((c1.toList, cAlias.toList))
var d1 = Array(1, 2)
val dAlias = d1
d1(0) = 9                 // kind 2: dAlias sees it
d1 = Array(5)             // kind 1: dAlias does not follow
println((d1.toList, dAlias.toList))
(Vector(1, 2, 3),Vector(1, 2))
(List(9, 2),List(9, 2))
(List(5),List(9, 2))

Why prefer immutable data (from the slides): easier to reason about, more flexible (you can keep every past version, as in Snake's reverse mode), fewer bugs (no hidden change, no forgotten copy), thread safe. The price: sometimes slower, since changing means building a new version.

The rule For each line that "changes" something, decide: is a name re-pointed (only that name is affected) or is an object modified (all aliases are affected)?
Trap Thinking val protects an array's contents, or thinking var plus immutable data means "the data changes". A var holding a Vector never changes the vector; it points to a new one.
On the exam The aliasing picture from Week 1 is the tool. Draw immutable boxes with a double border in your head: nobody can write inside them, so arrows to them are as good as copies.
Try it: predict the three lines above before reading the output

Line 1: b1 was re-pointed to a new vector; bOld still points to the old one. Line 2: one array, two names, a change inside it: both show 9. Line 3: dAlias sees the change made while d1 still pointed to that array, but not the re-pointing of d1 afterwards.

Immutable data: "changing" means making a new version

An immutable object has no operation that changes it. Methods that sound like changes (updated, :+, +, map, filter) return a new object and leave the old one exactly as it was. The slide puts it as: state of object = object. A new state is a new object.

This means a call like l.updated(1, "X") on its own line does nothing useful: it builds a new list and throws it away.

val l = List("a", "b", "c")
l.updated(1, "X")            // result thrown away
println(l)
val l2 = l.updated(1, "X")   // result kept
println(l)
println(l2)
List(a, b, c)
List(a, b, c)
List(a, X, c)

A pair: same program, immutable vs mutable map.

var m1 = Map("NL" -> "Amsterdam", "BE" -> "Antwerp")    // immutable Map
val m2 = m1
m1 = m1.updated("BE", "Brussels")       // new map; m1 re-pointed, m2 unchanged
println(m1("BE") + " " + m2("BE"))

val mm1 = scala.collection.mutable.Map("NL" -> "Amsterdam", "BE" -> "Antwerp")
val mm2 = mm1
mm1("BE") = "Brussels"                  // changes the one shared map
println(mm1("BE") + " " + mm2("BE"))
Brussels Antwerp
Brussels Brussels

Immutability is shallow. An immutable container only promises that its own slots never change. If a slot holds an arrow to a mutable object, that object can still be changed by anyone who has an arrow to it. updated copies arrows, not the objects behind them.

import scala.collection.mutable.ArrayBuffer
val buf = ArrayBuffer(1)
val v = Vector(buf, buf)                    // two arrows to ONE buffer
val v2 = v.updated(1, ArrayBuffer(100))     // new vector: slot 0 still the old buf
for (bb <- v2) bb += 5                      // mutates the buffers, not the vectors
println(v)
println(v2)
Vector(ArrayBuffer(1, 5), ArrayBuffer(1, 5))
Vector(ArrayBuffer(1, 5), ArrayBuffer(100, 5))

The same with a List of mutable objects. The val and the immutable List protect the list's structure, not the boxes it points to:

class Score(var points: Int)
def doubleAll(l: List[Score]): List[Int] = {
  l.foreach(s => s.points *= 2)       // mutates the caller's boxes
  l.map(_.points)
}
val scores = List(new Score(1), new Score(2))
println(doubleAll(scores))
println(scores.map(_.points))          // the original boxes were changed

case class CScore(points: Int)
def doubleAllC(l: List[CScore]): List[CScore] = l.map(s => CScore(s.points * 2))   // new boxes
val cscores = List(CScore(1), CScore(2))
println(doubleAllC(cscores))
println(cscores)                       // untouched
List(2, 4)
List(2, 4)
List(CScore(2), CScore(4))
List(CScore(1), CScore(2))

The key pair: an array of points, mutable vs immutable. The array is mutable in both programs; what differs is the point objects.

case class P(x: Int, y: Int) { def +(o: P): P = P(x + o.x, y + o.y) }   // immutable
val pa = new Array[P](4)
val start = P(1, 3)
for (i <- pa.indices) pa(i) = start              // 4 arrows to one immutable box
for (i <- pa.indices) pa(i) = pa(i) + P(0, i)    // each slot re-pointed to a NEW box
println(pa.map(_.y).sum)
println(start)

class MP(var x: Int, var y: Int) { def move(dx: Int, dy: Int): Unit = { x += dx; y += dy } }
val ma = new Array[MP](4)
val mstart = new MP(1, 3)
for (i <- ma.indices) ma(i) = mstart             // 4 arrows to one mutable box
for (i <- ma.indices) ma(i).move(0, i)           // all 4 moves go into that box
println(ma.map(_.y).sum)
println(mstart.y)
18
P(1,3)
36
9

Immutable: the slots end up with y = 3, 4, 5, 6 (sum 18), and the start point is unchanged. Mutable: the one box gets y = 3 + 0 + 1 + 2 + 3 = 9, and all four slots show it (sum 36).

"Depends on the definition of f". Some questions call a function f you cannot see and ask what is printed afterwards. Follow the path from the printed name to the value: if every step on that path is immutable, no f can change it. If some box on the path is mutable and f can reach it (it was passed in, or is global), the answer depends on f.

val arr = new Array[P](1)
arr(0) = P(5, 5)
val keep = arr(0)      // keep points to the immutable P(5,5)
arr(0) = P(10, 10)     // re-points the slot, not keep
// f(arr) here could only re-point slots of arr; it cannot touch P(5,5)
println(keep)
P(5,5)

Had we printed arr(0) after calling f(arr), the answer would depend on f, because an array slot can be re-pointed by anyone holding the array. Same for m(0)("key") when m is an Array[Map[...]]: the maps are immutable but the array is not.

The rule Operations on immutable data return new objects; the original is never affected. Ignoring the result means nothing happened. Immutability is shallow: mutable objects inside an immutable container can still change.
Trap (1) f(l) where f does l.updated(...) and returns Unit: the caller's list is unchanged. (2) Assuming an immutable Map or Vector makes the objects inside it immutable. (3) Assuming updated deep-copies. (4) Assuming immutable elements make the array that holds them immutable.
On the exam For each collection in the code, write next to it "mutable" or "immutable", and the same for the element class (case class with vals vs class with vars). Then use the box picture: immutable boxes can only be replaced, never written into.
Try it: what does this print?
var m1 = Map("NL" -> "Amsterdam", "BE" -> "Antwerp")
val m2 = m1
m1 = m1.updated("BE", "Brussels")
println(m1("BE") + " " + m2("BE"))

The map is immutable. updated builds a new map, and only m1 is re-pointed to it. m2 still points to the original: Brussels Antwerp (run above). With the mutable.Map version, both names see one map: Brussels Brussels.

Try it: what is the sum, immutable vs mutable?

In the point-array pair above, change the loops to add P(0, 2) (and move(0, 2)) in every iteration instead of i. Immutable: each slot gets its own point with y = 3 + 2 = 5, sum 20. Mutable: the one box gets four moves, y = 3 + 8 = 11, shown by 4 slots, sum 44. The general shape: immutable gives "start + one step" per slot; mutable gives "start + all steps" times the number of slots.

Try it: what does this print?
val buf = ArrayBuffer(1)
val v = Vector(buf, buf)
val v2 = v.updated(1, ArrayBuffer(100))
for (bb <- v2) bb += 5
println(v)
println(v2)

Draw it: v has two arrows to buf. v2 is a new vector with slot 0 = arrow to buf, slot 1 = arrow to a new buffer [100]. The loop over v2 appends 5 to buf and to the new buffer. v still has two arrows to buf, which is now [1, 5]. Output as shown above.

Immutable and mutable collections

Scala has both kinds in its library, in scala.collection.immutable and scala.collection.mutable. Without an import, List, Vector, Map, Set and Seq are the immutable ones. Array is always mutable.

mutableimmutable
Array, ArrayBuffer, ListBufferList, Vector
mutable.Set, mutable.Map, mutable.QueueSet, Map, immutable.Queue
a(i) = x (update), +=, ++=, addOne, remove: return Unit (or the same object)updated(i, x), :+, +:, +, ++, -: return a new collection

The slides compare the two interfaces. Read the result types:

class ArrayBuffer[A] {                       // mutable
  def apply(i: Int): A
  def update(index: Int, elem: A): Unit      // Unit: it must change this object
  def +=(elem: A): Unit
}
class Vector[+A] {                           // immutable
  def apply(i: Int): A
  def updated(index: Int, elem: A): Vector[A]   // returns the new version
  def :+(elem: A): Vector[A]
}

A method that "changes" something but returns Unit can only do its job by mutating the object, so it cannot exist on an immutable type. Query methods (contains, get, size, apply) do not change anything and exist on both. The mutable Set has the same interface as the immutable one plus extra mutating methods.

val vv = Vector(1, 2)
println(vv :+ 3)
println(0 +: vv)
println(vv.updated(0, 10))
println(vv ++ Vector(7, 8))
println(vv)                      // never changed
val im = Map(1 -> "one")
println(im + (2 -> "two"))
println(im - 1)
println(im.get(1))
println(im.get(5))
println(im)                      // never changed
Vector(1, 2, 3)
Vector(0, 1, 2)
Vector(10, 2)
Vector(1, 2, 7, 8)
Vector(1, 2)
Map(1 -> one, 2 -> two)
Map()
Some(one)
None
Map(1 -> one)

+= means two different things. On a mutable collection it is a method that changes the object. On a var holding an immutable collection, Scala rewrites s += 3 into s = s + 3: a new set, and the name is re-pointed.

var s = Set(1, 2)
val before = s
s += 3                     // s = s + 3
println(before)
println(s)
val ms = scala.collection.mutable.Set(1, 2)
val msBefore = ms
ms += 3                    // changes the one set
println(msBefore)
println(ms)
Set(1, 2)
Set(1, 2, 3)
HashSet(1, 2, 3)
HashSet(1, 2, 3)

A common idiom: build a result in a local ListBuffer (fast appends), then return buf.toList. The caller only ever sees the immutable list. This is "functional on the outside, imperative on the inside" (see pure functions).

Reading the type of a collection pipeline. Some questions ask the type of something like val f = l => l.map(_.map(_ + 1).sum).filter(_ % 2 == 0).sum. (Real Scala refuses this line with "missing parameter type", because l has no type; the question means "the type it must have".) Work from the operations to the layers:

  • _.map(_ + 1) applied to each element means each element is itself a collection of Int: so l is List[List[Int]].
  • map and filter keep the number of layers; flatMap and flatten remove one; sum, max, length, exists remove the collection entirely; map(x => List(x)) adds one.
val f1: List[List[Int]] => Int = ll => ll.map(_.map(_ * 2).sum).filter(_ > 3).sum
println(f1(List(List(1, 2), List(1), List(5))))
val f2: List[List[Int]] => List[Int] = ll => ll.filter(_.nonEmpty).flatMap(_.map(_ + 1))
println(f2(List(List(1, 2), List(), List(5))))
val f3: List[List[Int]] => List[List[Int]] =
  ll => ll.map(_.filter(_ > 1)).filter(_.length >= 1).map(x => List(x.max))
println(f3(List(List(1, 2, 3), List(1), List(5))))
16
List(2, 3, 6)
List(List(3), List(5))

These compile with exactly these type annotations, which confirms the layer counting.

The rule Immutable collections answer "change" requests with a new collection; mutable ones change themselves and return Unit. A Unit-returning add/delete method cannot belong to an immutable type.
Trap Accepting def add(elem: A): Unit for an immutable Set because "it returns nothing, so it changes nothing". It is the opposite: returning nothing means its only purpose is to change the object. Also rejecting get or contains because they "do not return a new set": queries are fine on both kinds.
On the exam "Which methods would you expect on an immutable Set/Map?" Go through the options: queries (return a value) yes; operations returning a new Set[A]/Map[K,V] yes; anything named add/delete/addAll/clear returning Unit no.
Try it: which methods fit an immutable Queue[A]?

(a) def enqueue(a: A): Queue[A], (b) def front: A, (c) def dequeue(): A (gives the front element and removes it), (d) def clear(): Unit, (e) def rest: Queue[A].

Yes: (a) returns a new queue, (b) is a query, (e) returns a new queue without the front. No: (c) must change the queue to remove the element (like the mutable Stack.pop() on the slides; the immutable SStack splits it into top and pop: SStack[A]), and (d) returns Unit.

Try it: what is the type?

l => l.filter(_.length > 3).flatMap(_.map(_ + 1)).filter(_ <= 2). _.length and _.map(_ + 1) on elements: elements are List[Int], so l is List[List[Int]]. filter keeps that; flatMap removes a layer: List[Int]; the last filter keeps it. Type: List[List[Int]] => List[Int], the same shape as f2 above. With that annotation it compiles, and applied to List(List(0, 1, 5, 1), List(0)) it gives List(1, 2, 2).

Case classes

A case class is a class for plain immutable data. Writing case in front of class gives you, for free:

  • constructor parameters are val fields (so the class is immutable by default);
  • a companion apply, so you write Point(1, 2) without new;
  • == that compares the fields (structural equality), with a matching hashCode, so it works as a Set element or Map key by value;
  • a readable toString: Point(1,2);
  • a copy method with named arguments: p.copy(y = 5);
  • support for pattern matching (Week 5).
class PlainPoint(val x: Int, val y: Int)
case class Point(x: Int, y: Int)
val p = Point(1, 2)
println(p)
println(p == Point(1, 2))                 // fields equal
val pp1 = new PlainPoint(1, 2)
val pp2 = new PlainPoint(1, 2)
println(pp1 == pp2)                       // plain class: different objects
val q = p.copy(y = 5)                     // new object, p unchanged
println((p, q))
val Point(a, b) = q                       // pattern: take the fields out
println(a + b)
println(Set(Point(1, 2), Point(1, 2)).size)
println(Set(pp1, pp2).size)
Point(1,2)
true
false
(Point(1,2),Point(1,5))
6
1
2

p.x = 3 does not compile ("reassignment to val"). "Immutable by default" is the precise phrase: you can write case class Counter(var n: Int), and then it is mutable:

case class Counter(var n: Int)
val cnt = Counter(1)
cnt.n += 1
println(cnt)
Counter(2)

A small taste of pattern matching, which is the main reason case classes exist:

def describe(pt: Point): String = pt match {
  case Point(0, 0) => "origin"
  case Point(x, 0) => "on the x-axis at " + x
  case _ => "somewhere else"
}
println(describe(Point(0, 0)) + "; " + describe(Point(4, 0)) + "; " + describe(q))
origin; on the x-axis at 4; somewhere else
The rule Case class = immutable by default, no new needed, == compares fields, nice toString, copy, usable in pattern matching. It is a normal, concrete class: not abstract and not automatically sealed.
Trap "Case classes are abstract" (false: you create them all the time) and "case classes are always sealed" (false: sealed is a keyword you put on a trait or abstract class, often the parent of case classes, like sealed abstract class CellType). Also: copy creates a new object, it does not change the old one.
On the exam Seeing case class Point(x: Int, y: Int) in an aliasing question tells you: the boxes are immutable, so sharing them is harmless, and every "+" makes a new box. Seeing class Point(var x: Int, var y: Int) tells you the opposite.
Try it: true or false?

(a) Case classes are immutable by default. (b) Case classes can be used in pattern matching. (c) Case classes are abstract. (d) Case classes are always sealed. (e) Two case class objects with equal fields are ==.

(a) true, (b) true, (c) false, (d) false, (e) true (see p == Point(1, 2) above).

Try it: what are the two set sizes?

Set(Point(1, 2), Point(1, 2)).size is 1 because case class equality compares fields, so the set sees one value twice. Set(pp1, pp2).size is 2 because plain objects are equal only to themselves (output above).

Side effects and side causes

A function has a main cause (its arguments, the inputs you see) and a main effect (its return value, the output you see). The professor's terms for everything else:

  • Side effect = hidden output. Anything the function does to the world other than returning a value: changing a variable outside the function (a global, a field), mutating an object that was passed in, printing, writing a file, "launching rockets".
  • Side cause = hidden input. Anything the result depends on other than the arguments: reading a global var or a field that can change, reading the contents of mutable data, the keyboard, a file, the clock, a random number generator.
inside the functionverdict
println(...), print(...)side effect
assign to a global var or a field of an outside objectside effect
mutate an argument: a(0) = 1, box.v += 1, buf.addOne(x)side effect
read a global var (even without writing it)side cause
read the contents of a mutable argument (a(0) of an array)side cause
Random.nextInt(), System.currentTimeMillis(), keyboard, fileside cause
create and change a local var, array or buffer that never escapesneither (invisible from outside)
call a function that has side effects/causesinherits them

Mutation is a side effect when anyone outside can see it. That is the link between Week 2's two halves: immutable data rules out a whole class of side effects.

Functions with memory. A function value (Week 3: closures) can capture a local var of the method that created it. That variable then lives on between calls, so calling the function changes hidden state: a side effect and a side cause at once. Each new call of the maker creates a new variable.

def makeCounter(): () => Int = {
  var count = 0
  () => { count += 1; count }
}
val c1 = makeCounter()
println(c1()); println(c1())
val c2 = makeCounter()          // a fresh count for c2
println(c2()); println(c1())    // c1 continues with its own count
1
2
1
3

Side effects make evaluation order visible. When an expression prints, it matters when and how often it is evaluated. That is why val, lazy val and def questions (Week 3 covers them in depth) use print: a val runs once at its definition, a lazy val once at its first use, a def at every use.

def show(tag: String, v: Int): Int = { print(tag + " "); v }
val v1 = show("val", 1)
lazy val l1 = show("lazy", 2)
def d1 = show("def", 3)
println("|")
val sum = d1 + l1 + d1 + l1 + v1
println("|")
println(sum)
val |
def lazy def |
11
The rule Side effect = hidden output (mutation visible outside, printing). Side cause = hidden input (reading changing state, randomness, time, input). Local mutation that nobody outside can see is neither.
Trap Thinking a function that only reads a global var has no side effects: it has a side cause, so it is still impure. And thinking a closure resets its captured variable on every call: only a new call of the maker does that. If the variable is global instead of local to the maker, all closures share it and nothing ever resets.
On the exam For each function, scan for two lists: what it reads besides its parameters, and what it writes or prints that outlives the call. Both lists empty: pure. For closure questions, draw the captured variable as a box next to the function, one box per call of the maker.
Try it: what does this print?
def make(start: List[Int]): Int => Int = {
  var q = start
  x => { val h = q.head; q = q.tail :+ x; h }
}
var r = make(List(7, 8))
print(r(1)); print(r(2)); print(r(3))
r = make(List(9))
print(r(4)); println(r(5))

Each call returns the current head and appends its argument. q: [7,8], r(1) gives 7, q = [8,1]; r(2) gives 8, q = [1,2]; r(3) gives 1, q = [2,3]. make(List(9)) creates a new q = [9]: r(4) gives 9, q = [4]; r(5) gives 4.

78194
Try it: the same, with q global
var q = List(7, 8)
def make(): Int => Int = x => { val h = q.head; q = q.tail :+ x; h }
var r = make()
print(r(1)); print(r(2)); print(r(3))
r = make()
print(r(4)); println(r(5))

Now there is only one q for all functions. The second make() does not reset it, so it continues from [2,3]: r(4) gives 2, r(5) gives 3.

78123
Try it: what does this print?
var n = 10
def bump(): Int = { n += 1; n }
lazy val a = bump()
val b = bump()
def c = bump()
println((c, a, b, a, c))

val b runs at its definition: n = 11, b = 11. The tuple is evaluated left to right: c runs bump: 12. a is forced now: 13. b is 11. a is cached: 13. c runs again: 14.

(12,13,11,13,14)

Pure functions

A mathematical function f : A -> B maps each input to exactly one output: same input, same output. A pure function is a mathematical function in this sense and has no side effects and no side causes. In the professor's words: functional programming = more pure functions, imperative programming = more impure functions, and the advice is "pure when possible, impure when needed".

Paper method: the purity checklist.

  1. Does the result depend on anything other than the arguments (global var, mutable array contents, random, time, input)? Then impure (side cause).
  2. Does it print, or change anything that still exists after it returns (globals, fields, the contents of an argument)? Then impure (side effect).
  3. Does it call an impure function? Then impure.
  4. Otherwise pure, even if it uses local vars, while or for loops, local arrays or ListBuffers, return, or recursion.

That last point is the slide "use of impure constructs does not have to leak": a sort that uses an array internally is still pure. Functional on the outside, imperative on the inside.

var total = 0
def addToTotal(x: Int): Int = { total += x; total }       // side effect + side cause
println(addToTotal(5)); println(addToTotal(5))           // same input, different output

def peek(arr: Array[Int]): Int = arr(0)                    // side cause: mutable contents
val arr = Array(1)
println(peek(arr)); arr(0) = 50; println(peek(arr))       // same argument, different output

def bump(arr: Array[Int]): Int = { arr(0) += 1; arr(0) }  // side effect on the argument
val arr2 = Array(1)
println(bump(arr2)); println(bump(arr2))

def sumList(l: List[Int]): Int = {                         // pure: local var only
  var res = 0
  for (e <- l) res += e
  res
}
println(sumList(List(1, 2, 3))); println(sumList(List(1, 2, 3)))

def evens(n: Int): Seq[Int] = {                            // pure: local buffer only
  val buf = scala.collection.mutable.ListBuffer[Int]()
  for (i <- 0 until n) if (i % 2 == 0) buf += i
  buf.toList
}
println(evens(7)); println(evens(7))
5
10
1
50
2
3
6
6
List(0, 2, 4, 6)
List(0, 2, 4, 6)

Classifying typical exam functions.

functionpure?why
def id(a: Int) = { println(a); a }noprints: a hidden output, even though it is a mathematical function
sum with a local var resyesthe var is local
sum with a global var res, reset to 0 at the startnowrites a global (others can see res afterwards)
def up(a: Array[Int]): Int = a(0)noreads mutable data: a side cause
def incAll(a: Array[Int]) = { ...a(i) = a(i)+1...; a }nomutates its argument, even though it returns it
l.map(_ + 1), l.filter(...), v.updated(0, 42) on Vector, .toStringyesnew values, nothing changed
n + Random.nextInt(), System.currentTimeMillis()noside causes
a loop with a local var that may run forever for some inputsyespurity is not the same as "always terminates"
local helper that flips a local var b, used inside a foryesall state is created inside each call
method of a case class reading only its own val fieldsyesthis is just an extra (immutable) argument
appleSpot(rand: RandomGenerator)nothe generator has hidden changing state
def s(...): Unit = ...only if it does nothinga Unit result means its only purpose is a side effect

What purity guarantees, and what it does not. Purity guarantees: evaluating f(x) twice gives the same result and changes nothing; f prints nothing; equal inputs give equal outputs; laws like xs.map(f).map(f) == xs.map(x => f(f(x))) hold. Purity says nothing about the mathematical shape of f:

val plus1: Int => Int = _ + 1          // pure
println((plus1(plus1(3)), plus1(3)))      // f(f(x)) == f(x)?  no: not idempotent
println((plus1(2 + 3), plus1(2) + plus1(3)))   // f(x+y) == f(x)+f(y)?  no
val zero: Int => Int = _ => 0           // pure
println((zero(1), zero(2)))               // f(x) == f(y) implies x == y?  no
(5,4)
(6,7)
(0,0)

One more guarantee that the exam likes: a call to a pure function can be moved past impure calls. In val y = f(x); val z = g(x); h(y, z) with pure f, swapping the first two lines changes nothing, because f neither reads what g might change nor changes what g might read.

The rule Pure = same input gives same output, and no side effects or side causes. What happens inside (local vars, loops, buffers) does not matter; only what can be seen from outside.
Trap (1) "It uses var / while / a ListBuffer, so it is impure": wrong if they are local. (2) "It only reads the array, so it is pure": wrong, mutable contents are a side cause. (3) "It returns the immutable list it built, so it is pure": wrong if it also mutated its argument on the way. (4) "Pure means idempotent / additive / one-to-one": no.
On the exam "Which functions are pure?" and "which statements about f are true?" come every year. Run the checklist per option; for "uses mutation internally" and "changes the input" statements, answer them separately: a function can use mutation and still be pure.
Try it: pure or not?
var factor = 2
def scale(x: Int): Int = x * factor
def scale2(x: Int, factor: Int): Int = x * factor
println(scale(5)); factor = 3; println(scale(5))
println(scale2(5, 3)); println(scale2(5, 3))

scale reads the global factor: a side cause, so the same call scale(5) gives 10 and then 15. Impure. In scale2, factor is a parameter (it shadows the global), so it depends only on its arguments: pure.

10
15
15
15
Try it: which statements are true?
def incr(a: Array[Int]): List[Int] = {
  for (i <- a.indices) a(i) += 1
  val res = scala.collection.mutable.ListBuffer[Int]()
  for (e <- a) res.addOne(e * 2)
  res.toList
}

(a) incr is pure: false, the first loop changes the caller's array. (b) incr uses mutation: true. (c) incr changes its input: true. (d) incr could be written with exists: false, exists returns a Boolean, not a list. Delete the first loop and (a) becomes true while (b) stays true (the local buffer).

Referential transparency

An expression is referentially transparent if you can replace it by its result without changing the meaning of the program. Math.sin(0) can be replaced by 0.0 everywhere. printId(9), which prints and returns 9, cannot be replaced by 9: you would lose the printing. Calls to pure functions on immutable values are referentially transparent; calls with side effects or side causes are not.

def printId(a: Int): Int = { println("printing " + a); a }
val r1 = printId(9) + printId(9)    // two prints
val nine = printId(9)
val r2 = nine + nine                // one print
println((r1, r2))
printing 9
printing 9
printing 9
(18,18)

The values are equal but the programs are not: one printed twice, the other once. So "compute once and reuse" is only safe for referentially transparent expressions.

Even e == e can be false. If evaluating e changes or reads changing state, the two sides of == are two different evaluations:

var n = 0
def next(): Int = { n += 1; n }
println(next() == next())          // 1 == 2
val once = next()
println(once == once)              // one evaluation, compared with itself
val dbl: Int => Int = _ * 2        // pure
n = 0
println(dbl(next()) == dbl(next()))  // pure outside does not help: 2 == 4
println(dbl(dbl(4)) == dbl(dbl(4)))  // only pure calls: always true
false
true
false
true

So, with f pure and g possibly impure: f(g(x)) == f(g(x)) may be false (a pure f faithfully maps two different g results to two different values), g(f(x)) == g(f(x)) may be false (g is still called twice), and f(f(x)) == f(f(x)) is always true.

Rewriting a function with pure and impure calls. Questions show a function like m below and ask which rewrites always give the same result. The rules: you may reuse, duplicate, or move calls to the pure function. You may not change the number or order of calls to the impure function, and you may not reuse one impure result in place of a second call. And purity does not relate different arguments: p(x + 1) is not p(x) + 1.

var calls = 0
def im(x: Int): Int = { calls += 1; x * calls }     // impure
def pz(x: Int): Int = x * 10                        // pure
def m(x: Int): Int  = { val a = pz(x); val b = im(x); val c = pz(x); a + b + c }
def m1(x: Int): Int = { val b = im(x); 2 * pz(x) + b }        // pure calls merged and moved
def m2(x: Int): Int = { val a = pz(x); val b = im(x); a + b + a }   // pure result reused
def m3(x: Int): Int = { val b = im(x); b + b + pz(x) }        // WRONG: im result used twice
def m4(x: Int): Int = pz(x) + im(x) + pz(x + 0)               // same calls, same order
// each called three times with x = 1, counter reset before each:
m: List(21, 22, 23)
m1: List(21, 22, 23)
m2: List(21, 22, 23)
m3: List(12, 14, 16)
m4: List(21, 22, 23)
The rule An expression is referentially transparent when replacing it by its value changes nothing. Pure calls are; impure calls are not. Pure calls may be reused, duplicated and moved; impure calls must keep their exact number and order.
Trap "An expression compared with itself is always true": only if it is referentially transparent. "Wrapping an impure call in a pure function makes it safe": no. "Two impure calls with the same argument can share one result": no, that is exactly what changes the output.
On the exam For each candidate rewrite, list the impure calls in order (im, im, ...). If that list differs from the original, the candidate is wrong. If it is the same, check that the pure calls compute the same values (same arguments) and that the final arithmetic matches.
Try it: which rewrites are equivalent?

Original: def k(x: Int) = { val a = im(x); val b = pz(x); val c = im(x); a + b + c + b }.
(a) { val a = im(x); val c = im(x); a + 2 * pz(x) + c }: impure calls im, im in the same order, pure values the same. Equivalent.
(b) { val a = im(x); a + pz(x) + a + pz(x) }: only one im call. Not equivalent.
(c) { val b = pz(x); im(x) + b + im(x) + b }: im, im in order (left-to-right evaluation), same values. Equivalent.
(d) { val a = im(x); val c = im(x); a + c + pz(2 * x) }: pz(2 * x) happens to equal 2 * pz(x) for this particular pz, but purity alone does not promise that. For an unknown pure p, not equivalent.

A run with the counter im from above and x = 2, three calls each: the original, (a), (c) and (d) all give List(46, 54, 62), (b) gives List(44, 48, 52). With the pure p(x) = x * x + 1 instead of pz, the original gives List(16, 24, 32) and (d) gives List(23, 31, 39).

Try it: true or false for all inputs?

f is pure, g may be impure. (a) f(f(x)) == f(f(x)), (b) g(g(x)) == g(g(x)), (c) f(g(x)) == f(g(x)), (d) g(f(x)) == g(f(x)).

(a) always true. (b), (c), (d) can be false: each side calls g on its own, and g may return something different the second time. The dbl(next()) run above is a concrete case of (c).

Equational reasoning: laws for pure code

Equational reasoning means reasoning about programs like algebra: you replace equals by equals. Because a pure expression can be replaced by its value (referential transparency), you can rewrite pure code step by step and know the result does not change. The slide example: for all lists, (a ++ b) ++ c == a ++ (b ++ c).

The laws you need (f, g pure; p, q pure predicates, that is functions returning Boolean):

leftequals
l.map(f).map(g)l.map(x => g(f(x))) (note the order: f first, so g is outside)
l.filter(p).filter(q)l.filter(x => p(x) && q(x))
l.map(f).filter(p)l.filter(x => p(f(x))).map(f)
l.filter(p).exists(q)l.exists(x => p(x) && q(x))
l.filter(p).forall(q)l.forall(x => !p(x) || q(x)), also written l.forall(x => if (p(x)) q(x) else true)
for (x <- l if p(x)) yield f(x)l.filter(p).map(f)
l.filter(p).lengththe number of elements with p true (l.count(p))

And the non-laws: l.map(f).map(g) is not l.map(g).map(f), and l.filter(p).map(f) is not l.map(f).filter(p) (the filter would test different values).

val xs = List(1, 2, 3, 4, 5, 6)
val f: Int => Int = _ + 1
val g: Int => Int = _ * 3
val even: Int => Boolean = _ % 2 == 0
val big: Int => Boolean = _ > 3
println(xs.map(f).map(g) + " " + xs.map(x => g(f(x))))
println(xs.map(f).map(g) + " " + xs.map(g).map(f))
println(xs.filter(even).filter(big) + " " + xs.filter(x => even(x) && big(x)))
println(xs.map(g).filter(big) + " " + xs.filter(x => big(g(x))).map(g))
println(xs.filter(even).map(f) + " " + xs.map(f).filter(even))
List(6, 9, 12, 15, 18, 21) List(6, 9, 12, 15, 18, 21)
List(6, 9, 12, 15, 18, 21) List(4, 7, 10, 13, 16, 19)
List(4, 6) List(4, 6)
List(6, 9, 12, 15, 18) List(6, 9, 12, 15, 18)
List(3, 5, 7) List(2, 4, 6)

Lines 1, 3 and 4 are laws (equal). Lines 2 and 5 are the non-laws (different).

Paper method for "which are equivalent?"

  1. Types first. Does the candidate even have the same type? for (x <- l if f(x)) yield true is a List[Boolean], not a Boolean; a nested yield inside a yield gives a nested list where flatMap gives a flat one.
  2. Rewrite with the laws until both sides look alike. Check the order of composition (g(f(x)) vs f(g(x))), && vs ||, exists vs forall.
  3. Test small cases: an empty list, a list where the predicate holds for some but not all elements, and simple functions like _ + 1 and _ * 3. One counterexample kills a candidate.
println(List(4).map(f).filter(even).exists(big) + " " +
        List(4).exists(x => even(f(x)) && big(f(x))) + " " +
        List(4).exists(x => even(f(x)) || big(f(x))))
println(List(3, 4, 5, 6).filter(even).forall(big) + " " +
        List(3, 4, 5, 6).forall(x => !even(x) || big(x)) + " " +
        List(3, 4, 5, 6).forall(big))
false false true
true true false

The || candidate and the "forget the filter" candidate are each killed by one small list.

Loops versus library functions. The same method checks "which implementations equal this loop?":

def countIf[A](l: List[A], f: A => Boolean): Int = { var res = 0; for (e <- l) if (f(e)) res += 1; res }
println((countIf(xs, big), xs.filter(big).length, xs.count(big),
         xs.map(big).length, xs.map(x => if (big(x)) 1 else 0).sum))
def allOf[A](l: List[A], f: A => Boolean): Boolean = { for (e <- l) if (!f(e)) return false; true }
println((allOf(xs, big), xs.map(big).fold(true)(_ && _), xs.filter(big).length == xs.length))
println((allOf(List[Int](), big), List[Int]().forall(big), List[Int]().exists(big)))
println((List(1, 2).map(big).length > 0, List(1, 2).exists(big)))
(3,3,3,6,3)
(false,false,false)
(true,true,false)
(true,false)

xs.map(big).length is 6: map keeps the length, so it counts everything, and l.map(f).length > 0 only tests "is the list non-empty", ignoring f. On the empty list forall is true and exists is false, which is why fold(true)(_ && _) needs true as start value (with false it is always false).

Laws break for impure functions. With side effects, l.map(f).map(g) runs all f calls and then all g calls (f1 f2 f3 g1 g2 g3), while l.map(x => g(f(x))) interleaves them (f1 g1 f2 g2 f3 g3). The hidden state is different at each call:

var k = 0
def fi(x: Int): Int = { k += 1; x * k }
def gi(x: Int): Int = { k += 10; x + k }
val l3 = List(1, 2, 3)
k = 0
val fused = l3.map(x => gi(fi(x)))
k = 0
val separate = l3.map(fi).map(gi)
println(fused)
println(separate)
List(12, 46, 102)
List(14, 27, 42)
fused: callk afterresultseparate: callk afterresult
fi(1)11fi(1)11
gi(1)1112fi(2)24
fi(2)1224fi(3)39
gi(24)2246gi(1)1314
fi(3)2369gi(4)2327
gi(69)33102gi(9)3342

Filters break the same way, with one extra twist: in filter(x => p(x) && q(x)), && short-circuits: q is not called at all when p is false. So even the number of calls differs.

var t = 0
def ping(x: Int): Boolean = { t += 1; t % 2 == 0 }
def pong(x: Int): Boolean = { t += 1; t % 3 == 0 }
val l4 = List(1, 2, 3, 4, 5, 6)
t = 0
println((l4.filter(ping).filter(pong), t))
t = 0
println((l4.filter(x => ping(x) && pong(x)), t))
(List(6),9)
(List(2, 5),11)
The rule With pure functions you may rewrite code with the laws (map fusion, filter fusion, for/yield as filter+map). With impure functions the laws fail, because the number and order of calls change.
Trap (1) Composition order: l.map(f).map(g) is g(f(x)), not f(g(x)). (2) Pure functions do not commute: map(f).map(g) is not map(g).map(f). (3) || instead of &&, forall instead of exists. (4) In impure questions the shared counter is not reset between the two parts of a tuple unless the code says so: trace one continuous timeline.
On the exam Two kinds of questions. "Which are equivalent (functions pure)?": use the laws plus a counterexample list. "What is the value (functions impure)?": forget the laws, make a call-by-call table with the hidden variable, as above.
Try it: trace the two filters

For the ping/pong run: two filters: ping is called for all six elements (t = 1..6), true at t = 2, 4, 6, so 2, 4, 6 survive. Then pong for those three (t = 7, 8, 9), true at t = 9: only 6 survives, t = 9.
One filter: x=1: ping t=1 false, pong skipped. x=2: ping t=2 true, pong t=3 true: keep 2. x=3: ping t=4 true, pong t=5 false. x=4: ping t=6 true, pong t=7 false. x=5: ping t=8 true, pong t=9 true: keep 5. x=6: ping t=10 true, pong t=11 false. Result List(2, 5), t = 11.

Try it: which are equivalent to l.filter(p).map(f).exists(q) (all pure)?

(a) l.exists(x => p(x) && q(f(x))): yes, by the filter/exists law after moving the map inside.
(b) l.map(f).filter(p).exists(q): no, p would test f(x) instead of x (like line 5 of the law run).
(c) (for (x <- l if p(x)) yield f(x)).exists(q): yes, the for is l.filter(p).map(f).
(d) l.forall(x => p(x) && q(f(x))): no, that demands every element instead of one.

Checked with p = even, f = _ + 1, q = _ > 3: on List(3) the original is false but (b) is true; on 1 to 6 the original is true but (d) is false; (a) and (c) agree with the original on every test list, including the empty one.

Try it: which equal l.forall(f)?

(a) a loop that returns false at the first element failing f and true at the end: yes (allOf above). (b) l.map(f).fold(true)(_ && _): yes. (c) l.filter(f).length == l.length: yes, nothing was removed means everyone passed. (d) l.filter(!f(_)).length == 0: yes, nobody failed. (e) l.map(f).fold(false)(_ && _): no, always false. The run above checks (a), (b), (c) and the empty list.

Readable code (assignment grading, and why the exam code looks the way it does)

The professor's framing: assignments train you to write readable code, the exam trains you to read unreadable code. Exam programs deliberately use names like m1, b, nub and reuse names to test your scoping skills. For your own code, and for possible statement questions, these are the points from the slides:

  • Easy to understand is hard to make. Code is mainly for people; readability beats performance in most contexts. Technical debt (quick and dirty now) costs more later.
  • Constants instead of magic numbers: CellWidthInPixels instead of 10. Easier to read and to change in one place (avoids "shotgun surgery": one conceptual change, many edits).
  • Meaningful, pronounceable names that say why something exists and how it is used. Short names for small scopes, longer names for large scopes. Standard abbreviations (gcd) are fine.
  • Functions do one thing, at one level of abstraction, and their name mentions every effect (checkPassword that also initialises a session is a hidden side effect).
  • Comments explain what code cannot (why, references to papers or bug numbers), never restate the code.
  • Code smells: duplicated code, long functions, deep nesting, large classes, inappropriate intimacy with another class's internals.
  • Common flaws (lecture 2): premature optimisation (needs a measurement), premature generalisation (YAGNI: "you ain't gonna need it"), reinventing the wheel (use the library's lists and sorts), global variables, the boolean antipattern if (bla) true else false instead of just bla, and not grouping data that belongs together into a class (Coordinate instead of separate x and y, a GameState class instead of a tuple of loose values). Mantras: DRY, KISS, YAGNI.
The rule Good code explains itself so well that it seems obvious when reading. Pure functions and immutable data help, because a reader never has to look for hidden changes.
On the exam No past exam question tests these points directly. They explain why the exam code is unreadable on purpose, so read it slowly and label every name.

Recap: Week 2 in one screen

conceptremember
two kinds of changere-point a name (only that name) vs mutate an object (every alias); val/var is about the name, mutable/immutable about the object
immutable data"change" returns a new object; ignoring the result = nothing happened; immutability is shallow
collectionsmutable: Array, ArrayBuffer, ListBuffer, mutable.Map/Set; immutable: List, Vector, Map, Set, Seq; a Unit-returning add/delete cannot be on an immutable type
+=method on mutable collections; on a var with an immutable value it means x = x + ...
pipeline typesmap/filter keep layers, flatMap/flatten remove one, sum/max/length/exists remove the collection
case classesval fields (immutable by default), no new, == on fields, toString, copy, pattern matching; not abstract, not sealed
side effect / side causehidden output (mutation seen outside, printing) / hidden input (reading changing state, mutable contents, random, time)
pure functionsame input, same output, no side effects or causes; local vars, loops and buffers are fine; not the same as idempotent or total
referential transparencyreplace an expression by its value without changing the program; pure calls can be reused, duplicated, moved; impure calls keep number and order
equational reasoningmap fusion g(f(x)), filter fusion with &&, for/yield = filter+map; check types, empty list, a counterexample; laws fail for impure functions (trace the call order)
readable codeconstants, names, one thing per function, one level of abstraction, comments for the why; YAGNI, DRY, KISS

Week 3: Advanced functions

This week functions stop being just "named pieces of code" and become values: you can store them in variables, pass them to other functions, return them, and they can remember variables from the place where they were made. The exam loves this block because tiny changes (a var instead of a val, a def instead of a lazy val, a comma instead of )() flip the answer. Every topic below gives you a paper method so you can trace the code instead of guessing.

Local functions and scope

A local function (the exam also says nested function) is a def written inside the body of another def. It only exists inside that body, just like a local variable. The slides give two reasons to use one:

  • It hides the helper from the rest of the program (a bit like private).
  • It saves argument passing: the local function can read (and change) the parameters and local variables of the function around it, so you do not have to pass them in.
def report(name: String, limit: Int): Unit = {
  def check(score: Int): Unit =
    if (score > limit) println(s"$name: $score")   // uses name and limit from outside
  check(40); check(75)
}
report("Ann", 50)
Ann: 75

check only takes score. It reads name and limit straight from report. Those are called the surrounding scope: the region of code where a name is visible.

Which variable does a name mean? (shadowing)

When two variables have the same name, the one declared closest around the use wins. The outer one is shadowed: still there, untouched, just not reachable by that name. A parameter also counts as a declaration. This is where most exam questions about local functions hide their trick.

Paper method: arrows, then a state table. For every name in the code, draw an arrow to the declaration it means (search outward: own parameters, own locals, enclosing function, then top level). Then run the code with one column per declaration (not per name).

var a = 10
var b = 1
def outer(n: Int): Int = {
  var a = n                                    // a NEW local a, shadows the top-level a
  def inner(b: Int): Int = { a += b; b * 2 }   // b is inner's parameter; a is outer's local a
  b += inner(b)                                // here b is the top-level b
  b += inner(b)
  a                                            // outer's local a
}
val r = outer(5)
println(r + " " + a + " " + b)
9 10 9
steptop atop blocal anote
start101-
var a = n1015new variable, top a untouched
b += inner(1)101 + 2 = 35 + 1 = 6inner returns 1*2
b += inner(3)103 + 6 = 96 + 3 = 9inner returns 3*2
return local a1099r = 9

Note that b += inner(b) means b = b + inner(b): the old b is read first, then inner runs. Here inner does not touch the top-level b (its own b is a parameter), so the order does not change anything.

The rule A local function can see and change every variable of the functions around it. A name always means the nearest enclosing declaration of that name; a parameter or a new var/val with the same name hides the outer one completely.
Trap Three classic slips: (1) letting the local function change the top-level variable when there is a local one with the same name; (2) reading a parameter z as the global z; (3) forgetting that the function returns its local variable while the caller then adds the global one. Delete one keyword and everything flips: if var a = n becomes a = n, there is no local a any more and the code changes the top-level one (see the first exercise).
On the exam "What is the output of main?" with a top-level var x, a function that declares its own var x, and a local function with a parameter named like a global. Attack: (1) draw the arrows for every occurrence of each name, (2) make one column per declaration, (3) run line by line. Also know the words: in statement questions, "g is a nested function" is true when g is a def inside another def.
Try it: the same program, but var a = n is replaced by a = n
var a2 = 10
var b2 = 1
def outer2(n: Int): Int = {
  a2 = n                                         // no var: assigns the top-level a2
  def inner(b2: Int): Int = { a2 += b2; b2 * 2 }
  b2 += inner(b2)
  b2 += inner(b2)
  a2
}
val r2 = outer2(5)
println(r2 + " " + a2 + " " + b2)
9 9 9

Now there is only one a2. It becomes 5, then 6, then 9, and outer2 returns that same variable. The top-level b2 goes 1, 3, 9 exactly as before. One missing keyword changed the middle number from 10 to 9.

Try it: when does a local function read the outer variable?
def g(): Int = {
  var base = 1
  def h(x: Int) = x + base
  val a = h(10)
  base = 5
  a + h(10)
}
println(g())
26

h reads base each time it runs, not when it is defined. First call: 10 + 1 = 11, stored in a. Then base becomes 5. Second call: 10 + 5 = 15. Result 11 + 15 = 26.

Try it: a parameter that shadows a global
var y = 100
def outer(y: Int): Int = { def inner(x: Int) = x + y; inner(1) }
println(outer(2) + y)
103

Inside outer, y means the parameter (2), so inner(1) is 3. Outside, in the println, y is the global 100. 3 + 100 = 103.

First class functions: functions are values

A value is anything you can put in a variable: 3, "hi", a List. Saying functions are first class means a function is a value too. The slides list what you can then do with it:

  • store it in a variable (var increase: Int => Int = plusOne),
  • pass it as an argument (list.map(f), button.onClick(f)),
  • return it as the result of a function (makeDiscount(0.9) returns a discount function),
  • keep it as a field of a class (class KeyBinding(val key: String, var action: () => Unit)).

Function types

Every function value has a function type, written with an arrow:

typeread it asexample value
Int => Inttakes one Int, gives an Int(x: Int) => x + 1
(Int, Int) => Inttakes two Ints at once, gives an Int(x: Int, y: Int) => x * y
() => Unittakes nothing, does something() => println("Jump!")
(Int => Int) => Inttakes a function, gives an Intf => f(0)
Int => Int => Inttakes an Int, gives a function Int => Intx => y => x + y (see currying)

Methods versus function values

A def double(x: Int): Int is a method. Scala turns it into a function value automatically when a function type is expected; this is called eta-expansion. If no function type is expected, Scala 2.13 refuses, and you write double _ or double(_).

def double(x: Int): Int = x * 2
val f1: Int => Int = double     // function type expected: fine
val f2 = double _                // explicit
val f3 = double(_)               // placeholder, same thing
println((f1(4), f2(4), f3(4)))

var op: (Int, Int) => Int = _ + _
println(op(3, 4))
op = _ * _                       // the variable now holds another function
println(op(3, 4))
(8,8,8)
7
12

Without the type annotation, val f = double does not compile: error: missing argument list for method double. Unapplied methods are only converted to functions when a function type is expected.

A function value of type () => Unit is only run when you write the brackets:

val hello: () => Unit = () => println("hi")
println(hello)     // prints the function object itself
hello()            // runs it
Main$$anon$1$$Lambda/0x000000d0014e5920@ee21292
hi

Data structures made of functions

The lecture builds a set as a function: type Set = Int => Boolean. A set is its membership test: "give me a number, I tell you whether it is in me". The exam reuses this idea for multisets (A => Int), maps (A => Option[B]), infinite lists (Int => A) and lists (case class FList(elems: Int => A, length: Int)). The method is always the same:

  1. Write down what the function type means in words ("given an index, the element at that index").
  2. The operation you have to build returns a new function, so its body is a lambda x => ... (or a local def that is returned).
  3. Inside, answer the question for one argument x by asking the old functions about x (or about a shifted index).
type ISet = Int => Boolean
val evens: ISet = _ % 2 == 0
def range(lo: Int, hi: Int): ISet = x => lo <= x && x < hi
def diff(a: ISet, b: ISet): ISet = x => a(x) && !b(x)
val s = diff(range(0, 10), evens)
println((0 to 12).filter(s).toList)
List(1, 3, 5, 7, 9)

A list as a function, with an index shift (the same trick as "prepend" or "merge" questions):

case class Seq2[A](at: Int => A, size: Int) {
  def append(e: A): Seq2[A] = Seq2(i => if (i == size) e else at(i), size + 1)
  def reverse: Seq2[A] = Seq2(i => at(size - 1 - i), size)
  def toList: List[A] = (0 until size).map(at).toList
}
val sq = Seq2[Int](i => i * 10, 3)
println((sq.toList, sq.append(7).toList, sq.reverse.toList, sq.append(7).reverse.toList))
(List(0, 10, 20),List(0, 10, 20, 7),List(20, 10, 0),List(7, 20, 10, 0))
The rule A function value can go anywhere a value can go. Its type A => B says what it takes and what it gives. A data structure built from functions has no elements you can loop over: you can only ask it about one argument at a time.
Trap Options that loop over a function-based set (for (x <- a)), call collection methods on it (a ++ b, .keys, .addOne), or build a real List/Array are wrong: a function has no elements. a compose b for union is also wrong: composition feeds the Boolean result of one set into the other, instead of asking both sets about the same x. For a multiset sum, a(_) + b(_) is wrong: those are two different parameters (see placeholders); you need x => a(x) + b(x). For "prepend", the length must grow by one and the old elements move: index(i - 1), not index(i).
On the exam "Which are valid implementations of union / sum / merge / map / prepend?" (multiple response). For each option ask: (1) does it return a function of the right type? (2) for one concrete x, does it give the right answer? Test with tiny inputs (a set that only contains 3, a list of length 2). Correct answers can look very different: a lambda, a local def that is returned, elems andThen f. Also a one-liner: "which language does not support first class functions?" Answer C (only function pointers, no lambdas or closures); Scala, Haskell, Lisp and Python all have them.
Try it: a phone book as a function String => Option[Int]
type Phone = String => Option[Int]
val empty: Phone = _ => None
def add(m: Phone, k: String, v: Int): Phone = x => if (x == k) Some(v) else m(x)
val book = add(add(empty, "ann", 1), "bob", 2)
val book2 = add(book, "ann", 9)
println((book("ann"), book("bob"), book("cy"), book2("ann"), book("ann")))
(Some(1),Some(2),None,Some(9),Some(1))

Ask book("ann"): the outer layer is the "bob" layer, "ann" != "bob", so it asks the inner map; that is the "ann" layer, answer Some(1). "cy" falls through every layer to empty: None. book2 checks its own "ann" layer first, so it gives Some(9), and the newest layer wins. book itself is unchanged: nothing was mutated, add made a new function.

Try it: an infinite list as a function
type Inf = Int => Int
val nat: Inf = i => i
def shift(a: Inf, k: Int): Inf = i => a(i + k)
def every2(a: Inf): Inf = i => a(2 * i)
println((0 to 4).map(every2(shift(nat, 1))).toList)
List(1, 3, 5, 7, 9)

Substitute from the outside in: every2(shift(nat,1))(i) = shift(nat,1)(2*i) = nat(2*i + 1) = 2*i + 1. For i = 0..4 that is 1, 3, 5, 7, 9.

Try it: union and intersection of function sets
type ISet = Int => Boolean
def single(e: Int): ISet = _ == e
def union(a: ISet, b: ISet): ISet = x => a(x) || b(x)
def inter(a: ISet, b: ISet): ISet = x => a(x) && b(x)
val u = union(single(3), x => x > 10)
println((u(3), u(4), u(11), inter(u, _ % 2 == 0)(12), inter(u, _ % 2 == 0)(3)))
(true,false,true,true,false)

u is "equals 3 or above 10". 12 is in u and even: true. 3 is in u but odd: false.

Lambdas (anonymous functions, function literals)

A lambda is a function written directly as an expression, without a name. The slides also call it an anonymous function or function literal, just like 3 is an Int literal.

(x: Int) => x * x          // parameter list, arrow, body
(x: Int, y: Int) => x + y  // two parameters
() => println("BOOM!")     // no parameters

You can apply a lambda directly, store it, or pass it:

println(((x: Int) => x * x)(7))
println(List(1, 2, 3).map(x => x * x))     // type of x inferred from the List
val describe: Int => String = n => {
  val half = n / 2                          // a block: the last expression is the result
  s"$n has half $half"
}
println(describe(9))
49
List(1, 4, 9)
9 has half 4

Parameter types. You may leave out : Int only when Scala already knows which function type is expected (from a type annotation, or from the parameter type of the method you pass it to). val g = x => x + 1 alone gives error: missing parameter type.

The body runs on every call. Anything declared inside the lambda body is created fresh each time the lambda is called. Anything declared outside it, but captured by it, lives on between calls. This pair shows the difference:

def mk(): Int => Int  = { var t = 0; x => { t += x; t } }   // t outside the lambda
def mk2(): Int => Int = x => { var t = 0; t += x; t }        // t inside the lambda
val m1 = mk(); val m2 = mk2()
println((m1(5), m1(5), m2(5), m2(5)))
(5,10,5,5)
The rule (params) => body is a value. The body is not run when the lambda is created, only when it is called, and it runs again from the top on every call. Where a var is written (inside or outside the arrow) decides whether it is reset per call.
Trap Thinking that a var x = v inside the lambda keeps its value between calls (it does not), or that applyAll(l) already computes a number (it only returns a function; nothing runs until you call it with an argument).
On the exam Lambdas appear in nearly every code question. In for-loop desugaring options (a week 4 topic) they appear as r => r.map(_ + 1): check which variable each lambda binds and whether a test is applied to the right level (rows or elements).
Try it: a lambda with a block body
val f: Int => Int = x => { val y = x * 2; y + 1 }
println(f(f(1)))
7

Inner call first: f(1) = 1*2 + 1 = 3. Then f(3) = 3*2 + 1 = 7. The val y is a new variable in each call.

Try it: does val g = x => x + 1 compile? And with (x: Int)?

The first does not: error: missing parameter type, because nothing tells Scala what x is. val g = (x: Int) => x + 1 compiles and gets type Int => Int. So does val g: Int => Int = x => x + 1.

Higher order functions

A higher order function is a function that takes a function as an argument, returns a function, or both. map, filter and exists take one. makeDiscount(0.9) returns one. The slides use two famous ones that the exam uses in almost every paper:

def andThen[A,B,C](f: A => B, g: B => C): A => C = x => g(f(x))   // first f, then g
def compose[A,B,C](g: B => C, f: A => B): A => C = x => g(f(x))   // g "after" f
val inc: Int => Int = _ + 1
val dbl: Int => Int = _ * 2
println(andThen(inc, dbl)(5))
println(compose(inc, dbl)(5))
println(((inc andThen dbl)(5), (inc compose dbl)(5)))   // the library versions
12
11
(12,11)

andThen(inc, dbl): first +1, then *2: (5+1)*2 = 12. compose(inc, dbl) means inc after dbl: 5*2+1 = 11. Do not trust the name: some exam versions define their own after, at or comp. Always look at the body. x => g(f(x)) runs f first, whatever the function is called.

Paper method: the pipeline

  1. For each helper, read its body once and write it as a left to right pipeline. andThen(f, g) becomes f ; g. after(f, g) with body f(g(x)) becomes g ; f.
  2. Expand from the inside out until you have one flat list of small steps. A repeat helper (iterate(f, 3), twice(f)) becomes f ; f ; f.
  3. Push the number through the list, writing every intermediate value.
def times[A](f: A => A, n: Int)(a: A): A = if (n == 0) a else times(f, n - 1)(f(a))
println(times(andThen(dbl, (_: Int) - 3), 3)(4))
11
steppipelinevalue
expand(*2 ; -3) ; (*2 ; -3) ; (*2 ; -3)start 4
round 14*2 = 8, 8-35
round 25*2 = 10, 10-37
round 37*2 = 14, 14-311

Count the repeats carefully. times(f, 3) applies f exactly 3 times (n goes 3, 2, 1, then 0 returns). for (i <- 0 until n) also runs n times, because until excludes n.

Recognise a higher order function by its shape

Some questions show a function with a meaningless name and ask "what is the name of this higher order function?". Look at the type and the body:

type / body shapename
(A => B, List[A]) => List[B], body f(head) :: rec(tail)map
f(head) && rec(tail), Nil => trueforall
f(head) || rec(tail), Nil => falseexists
l.map(x => if (p(x)) 1 else 0) then sum (returns a number)count
(A => B, B => C) => A => C, body g(f(x))andThen
(B => C, A => B) => A => C, body f(g(x))compose
((A,B) => C) => (B,A) => Cflip
((A,B) => C) => A => B => Ccurry
(A => B => C) => (A,B) => Cuncurry
(A => A) => A => A, body f(f(x))twice
The rule x => g(f(x)) runs f first. Read the body, never the name. Flatten everything into one left to right list of steps before you compute.
Trap The wrong options are built from three mistakes: applying the functions in reverse order, applying a repeated function one time too few or too many, and applying the final outer function on every round instead of once at the end. Also watch for helpers that swap their arguments on every recursive call (alt(g, f, i-1)(f(a))): then the functions alternate. And a "stop when" helper (reUntil, until) tests the new value before stopping, so it keeps the first value that passes. For the recognise-the-shape questions: forall on an empty list is true and exists is false; l.map(pred).length is just l.length (map never removes elements), so it does not count anything.
On the exam The last question of almost every paper is "What is the value of x?" with nested andThen/iterate/twice. Spend two minutes on the pipeline method; write the intermediate numbers. Statement questions ("f is curried / pure / recursive / tail recursive") also tag this topic: a function is recursive if it calls itself, tail recursive only if that call is the very last thing it does (nothing like z += f(...) or x + y after it).
Try it: two repeats in a row
println(andThen(times(inc, 2), times(dbl, 3))(1))
24

Pipeline: +1 ; +1 ; *2 ; *2 ; *2. 1, 2, 3, 6, 12, 24.

Try it: a list of functions chained recursively
def chain(fs: List[Int => Int]): Int => Int =
  if (fs.isEmpty) x => x else andThen(fs.head, chain(fs.tail))
println(chain(List(_ + 3, _ * 2, _ - 1))(1))
7

Unfold: andThen(+3, andThen(*2, andThen(-1, id))), so the pipeline is +3 ; *2 ; -1 ; id, in list order. 1+3 = 4, 4*2 = 8, 8-1 = 7. The identity at the end does nothing; it only stops the recursion.

Try it: functions that swap places
def swapEach[A](f: A => A, g: A => A, n: Int)(a: A): A =
  if (n == 0) a else swapEach(g, f, n - 1)(f(a))
println(swapEach[Int](_ + 10, _ * 2, 4)(1))
def until[A](f: A => A, stop: A => Boolean)(a: A): A = if (stop(a)) a else until(f, stop)(f(a))
println(until[Int](_ * 3, _ > 50)(2))
64
54

swapEach applies its first function and then calls itself with the two swapped: +10, *2, +10, *2. 1, 11, 22, 32, 64. until: 2 (not > 50), 6, 18, 54 (> 50, stop) gives 54.

Try it: name this function
def mystery[A](l: List[A], p: A => Boolean): Boolean = l match {
  case h :: t => p(h) || mystery(t, p)
  case Nil => false
}
println((mystery(List(1, 2, 3), (_: Int) > 2), mystery(List[Int](), (_: Int) > 2)))
(true,false)

|| plus false for the empty list: this is exists. With && and true it would be forall.

Placeholder (_) notation

The slides: "nth occurrence of _ = nth argument". It is a short way to write a lambda:

shortmeans
_ < 10x => x < 10
_ + 1x => x + 1
_ + _(x, y) => x + y (two parameters!)
_.forall(isEven)l => l.forall(isEven)
(_: Int) - (_: Int)(x: Int, y: Int) => x - y

Rule 1: every _ is a new parameter. You cannot use one parameter twice with underscores. _ * _ takes two arguments, so it fits reduce but not map:

println(List(1, 2, 3).map(_ * 10))
println(List(1, 2, 3).reduce(_ * _))     // reduce wants (Int, Int) => Int
val sub = (_: Int) - (_: Int)
println(sub(10, 3))
println(List(1, 2, 3).map(_ + -2))       // _ + -2 is x => x + (-2)
List(10, 20, 30)
6
7
List(-1, 0, 1)

List(1, 2, 3).map(_ * _) does not compile: type mismatch; found: (?, ?) => ? required: Int => ?.

Rule 2: how far does the lambda reach? Find the _ and walk outward. The lambda is the first enclosing argument (or right-hand side of =) that is bigger than the _ itself. If the _ is a whole argument on its own, like in g(_), the lambda grows one level further and includes the call g(...).

codeexpands to
l.map(_ + 1)l.map(x => x + 1)
l.map(math.abs(_) + 1)l.map(x => math.abs(x) + 1)
l.filter(_.forall(_ % 2 == 0))l.filter(r => r.forall(x => x % 2 == 0))
inc(_) + dbl(_)(x, y) => inc(x) + dbl(y)
l.map(dbl(inc(_)))l.map(dbl(x => inc(x))): a type error
println(List(-3, 2).map(math.abs(_) + 1))
println(List(List(2, 4), List(1)).filter(_.forall(_ % 2 == 0)))
val pairUp: (Int, Int) => Int = inc(_) + dbl(_)
println(pairUp(5, 1))
List(4, 3)
List(List(2, 4))
8

And the one that fails: List(1, 2).map(dbl(inc(_))) gives type mismatch; found: Int => Int required: Int, because dbl receives the function x => inc(x) instead of a number. To use a parameter in two places, or inside a nested call, write the lambda out: x => inc(x) + dbl(x), x => dbl(inc(x)).

The rule One _ = one fresh parameter, numbered left to right. The lambda is the smallest enclosing argument that contains more than just the _. If you need the same value twice, or f(g(x)), you need a named parameter.
Trap x => a(x) + b(x) and a(_) + b(_) look the same but are not: the second takes two arguments, so it is not an A => Int. Note on one past exam: a key once accepted elems = f(elems(_)) as a correct map. By rule 2 that is f(x => elems(x)), and Scala 2.13 rejects it (type mismatch; found: Int => A required: A). If you meet it, know that the intended meaning was i => f(elems(i)).
On the exam Placeholders are everywhere (_ * 2, _ - 1, _.length > 1). In "which implementations are correct" questions, count the underscores: the count must match the number of parameters of the expected function type.
Try it: how many parameters?

For each, give the expansion: (a) _ * 2 + _, (b) _.length > 1, (c) _ + _ + _.

(a) (x, y) => x * 2 + y, two parameters. (b) s => s.length > 1, one. (c) three parameters. Checked: val g: (Int, Int) => Int = _ * 10 + _; println(g(3, 4)) prints:

34
Try it: which compile?

(a) List(1,2,3).map(_ * _) (b) List(1,2,3).reduce(_ * _) (c) val bad: Int => Int = inc(_) + inc(_)

Only (b). (a) is a two-parameter function where map wants one. (c) also has two parameters; the compiler says missing parameter type for expanded function ((x$4, x$5) => inc(x$4).$plus(inc(x$5))) and found: (?, ?) => ? required: Int => Int.

Mutability and first class functions: closures

A lambda's body can mention variables it did not declare itself. In (x: Int) => x + diff, x is the lambda's own parameter, but diff comes from outside. The slides call diff a free variable: a variable introduced in an outer scope. The function value that exists at runtime is a closure: "the definition of the function + bindings of the free variables". The name comes from "closing" the function: giving its free variables a meaning.

The question the exam keeps asking is: what exactly is bound, the value or the variable? The slides answer it: "Variable itself is being captured, not its value."

Rule 1: a captured var is read when the function runs

var rate = 2
val pay: Int => Int = h => h * rate
println(pay(10)); rate = 3; println(pay(10))
20
30

The closure holds a link to the variable rate, not a copy of 2. At the second call the variable holds 3. Same code, one small change: capture a val copy instead.

var rate2 = 2
val frozen = rate2                  // copies the current value 2
val pay2: Int => Int = h => h * frozen
println(pay2(10)); rate2 = 3; println(pay2(10))
20
20

Parameters are vals too. In def adder(n: Int): Int => Int = x => x + n the value of n can never change, so adder(5) always adds 5. And a result that was already computed is just a number: after val snapshot = addK(0), changing k does not change snapshot.

Rule 2: each call of the maker creates a new variable

When a function declares a var and returns a lambda that uses it, every call of that function makes a new var and a new closure linked to it. The closure keeps that variable alive after the function has returned. Calling the closure changes its own variable and nobody else's.

def counter(): () => Int = { var c = 0; () => { c += 1; c } }
val c1 = counter()
val c2 = counter()
var c3 = c1                     // NOT a copy: c3 and c1 point to the same closure
println((c1(), c1(), c2(), c3(), c1()))
c3 = counter()                  // c3 now points to a brand new closure
println((c3(), c1()))
(1,2,1,3,4)
(1,5)

Paper method: boxes and arrows. Draw a box for every variable that a closure captures, created at the moment the maker runs. Draw every closure as a circle with an arrow to its box. Variables like c1 hold arrows to circles. Assignment c3 = c1 copies the arrow, never the circle or the box.

c1c3c2 #1#2 c = 4c = 1 box of call 1box of call 2

After the first line: closure #1 was called three times (twice through c1, once through c3), so its box went 1, 2, then 3 via c3, then 4. Closure #2 was called once.

Rule 3: where the var lives decides who shares it

the var is declaredresult
inside the maker, outside the lambdaone variable per maker call; state survives between calls of that closure; a new maker call starts fresh
inside the lambda bodyfresh on every call; no state at all
at top level (global), outside the makerone variable for everyone; a new maker call does not reset it
var shared = 100
def mkShared(): () => Int = () => { shared += 1; shared }
val s1 = mkShared(); val s2 = mkShared()
println((s1(), s2(), s1()))
(101,102,103)

Another way to see it: a loop that captures

A for (i <- 1 to 3) loop makes a new val i for every round. A while loop with one var j has one variable for all rounds. If closures capture the loop variable, you see the difference when you call them later:

import scala.collection.mutable.ArrayBuffer
val fs = ArrayBuffer[() => Int]()
for (i <- 1 to 3) fs += (() => i)
println(fs.map(f => f()).toList)

val gs = ArrayBuffer[() => Int]()
var j = 1
while (j <= 3) { gs += (() => j); j += 1 }
println(gs.map(g => g()).toList)
List(1, 2, 3)
List(4, 4, 4)

All three closures in gs share the single j, and they read it when they are called, after the loop ended with j = 4.

The same rule for objects

A closure with a captured var is just an object with a private field. Iterators in past questions behave the same way: every call of r.iterator (or new PrmIterator()) creates a new object with its own cur, while val it2 = it1 is a second name for the same object, so both names advance one shared cursor. And a val that holds a mutable object (like an ArrayBuffer or an Array) still lets a closure change what is inside that object.

The rule A closure captures the variable, not its current value: a captured var is read (and written) at call time. Every call of the function that declares the var creates a new, independent variable. Assigning a closure to another variable shares it; it never copies the state.
Trap The professor's favourite wrong answers: (1) "the closure froze x when it was defined" (it did not, for a var); (2) "the counter resets on every call of g" (only on every call of f); (3) "g = g or v = w makes a copy" (it copies the arrow); (4) "the second closure continues where the first stopped" (only if the var is global). Spot the location of the var first. A related trap: in def f(x: Int): Int => Int = { var a = x; x => { a += x; a } } the lambda's own parameter x shadows f's x inside the body.
On the exam "What does m() print?" with a maker f(), a var g = f(), a few calls, then g = f() and more calls. Attack: (1) find every var/mutable object the lambda uses and where it is declared; (2) for each maker call draw a new box; (3) make a table: call, box contents before, value returned, box after. Remember that the function usually saves the old value (val h = l.head, val res = s(i)) before updating, and returns that old value.
Try it: a closure that rotates a list
def rotor(start: List[Int]): Int => Int = {
  var q = start
  x => { val out = q.head; q = q.tail :+ x; out }
}
val ro = rotor(List(7, 8))
println((ro(1), ro(2), ro(3), ro(4)))
(7,8,1,2)
callq beforereturnsq after
ro(1)List(7,8)7List(8,1)
ro(2)List(8,1)8List(1,2)
ro(3)List(1,2)1List(2,3)
ro(4)List(2,3)2List(3,4)

The list itself is immutable; the captured var q is reassigned to a new list each time. If var q were a global variable, a second rotor(...) call would not reset it.

Try it: an array window
def window(n: Int): Int => Int = {
  val a = new Array[Int](n)
  var i = 0
  x => { a(i) = x; i = (i + 1) % n; a.sum }
}
val w = window(2)
println((w(5), w(6), w(7)))
(5,11,13)

Array starts [0,0]. w(5): [5,0], sum 5. w(6): [5,6], sum 11. w(7): i wrapped to 0, [7,6], sum 13. The val a never changes, but the array it points to does.

Try it: def versus val holding a closure
def tickDef: () => Int = { var c = 0; () => { c += 1; c } }
val tickVal: () => Int = { var c = 0; () => { c += 1; c } }
println((tickDef(), tickDef(), tickVal(), tickVal()))
(1,1,1,2)

A def runs its body each time it is used, so every tickDef() builds a new counter and calls it once: always 1. The val ran its body once, so both calls use the same counter. (This links to laziness, below.)

Try it: shadowed parameter inside the lambda
def acc(x: Int): Int => Int = {
  var total = x
  x => { total += x; total }
}
val ac = acc(100)
println((ac(1), ac(2)))
(101,103)

total starts at the outer x = 100. Inside the lambda, x is the lambda's argument: 100 + 1 = 101, then 101 + 2 = 103.

Nested classes

Just as you can put a def inside a def, you can put a class inside a class. The slides: "exact same rules". An object of the inner class can read and change the fields and methods of the outer object it was created from.

The key word is object. You create an inner object through a specific outer object: new ajax.Player(9). That inner object stays linked to ajax forever. Its type is ajax.Player; the slides call this a path dependent type: the type of an inner class depends on the enclosing object.

class Team(val name: String) {
  var goals = 0
  class Player(val nr: Int) {
    var own = 0
    def score(): Unit = { own += 1; goals += 1 }   // goals = the goals of MY team
  }
}
val ajax = new Team("Ajax"); val psv = new Team("PSV")
val p1 = new ajax.Player(9); val p2 = new ajax.Player(10); val p3 = new psv.Player(7)
p1.score(); p2.score(); p1.score(); p3.score()
println((ajax.goals, psv.goals, p1.own, p2.own, p3.own))
(3,1,2,1,1)

Paper method: boxes with an "outer" arrow. Draw a box per outer object with its fields. Draw a box per inner object with its own fields plus an arrow labelled "outer" to the box it was created from. A name in the inner class body that is not its own field follows the outer arrow. Variables that are aliases (val t2 = ajax) are just two arrows to the same box.

Team ajaxgoals = 3 Team psvgoals = 1 p1: own = 2p2: own = 1p3: own = 1 outer

Each inner object has its own inner fields (own), and all inner objects of the same outer object share that outer object's fields (goals). Different outer objects (ajax, psv) have separate fields.

Aliasing the outer object does not make a new one:

val t2 = ajax
val p4 = new t2.Player(11)
p4.score()
println((ajax.goals, t2.goals, psv.goals))
(4,4,1)

The types really are different per object: val p: ajax.Player = new psv.Player(7) gives error: type mismatch; found: this.psv.Player required: this.ajax.Player.

Inner objects that change a function field of the outer object

This combines nested classes with closures. Watch the line val old = run: it saves the current function value before replacing it.

class Pipeline(var run: Int => Int) {
  class Stage(val step: Int => Int) {
    def attach(): Unit      = { val old = run; run = x => step(old(x)) }   // old first, then step
    def attachFront(): Unit = { val old = run; run = x => old(step(x)) }   // step first, then old
  }
}
val pa = new Pipeline(_ * 10)
new pa.Stage(_ + 1).attach()
new pa.Stage(_ * 2).attach()
println(pa.run(1))
val pb = new Pipeline(_ * 10)
new pb.Stage(_ + 1).attachFront()
new pb.Stage(_ * 2).attachFront()
println(pb.run(1))
22
30

pa: run becomes *10 ; +1, then *10 ; +1 ; *2: 1, 10, 11, 22. pb: each new stage goes in front: +1 ; *10, then *2 ; +1 ; *10: 1, 2, 3, 30. Both stages belong to the same outer object, so they both change the same run.

Now delete val old = run and write run = x => step(run(x)). The lambda captures the variable run, reads it at call time, and by then run is the lambda itself. It calls itself forever:

StackOverflowError
The rule An inner object is tied to the outer object it was created through. Its own fields are per inner object; the outer fields it uses are shared by all inner objects of that same outer object. Aliases of the outer object are the same object.
Trap Giving each inner object its own copy of the outer field (wrong: they all update the outer one); thinking val a2 = a has its own x (it is the same object); thinking that reassigning b = new a.B(4) undoes what the old b already did (it does not, the outer field was changed for good); and treating copyCell(c), which calls new Cell(c.v), as an alias (it is a new object with a copied number). Count list entries, not distinct objects: List(c1, c2, c2) pokes c2 twice.
On the exam "What does main print?" with an outer class holding a var (an Int or a function) and inner objects that update it, sometimes three levels deep (p.Q, q.R). Attack with the box method: one box per new, an outer arrow from each inner box, then execute line by line, updating the box the arrow leads to. For function fields, write the current pipeline after every update.
Try it: three levels deep
class Bank(var total: Int) {
  class Account(var bal: Int) {
    class Card(val limit: Int) {
      def pay(): Int = { bal -= limit; total -= limit; total }
    }
  }
}
val bk = new Bank(100)
val acc = new bk.Account(50)
val card = new acc.Card(10)
println(card.pay())
println(card.pay())
val card2 = new acc.Card(5)
println(card2.pay())
val acc2 = new bk.Account(20)
val card3 = new acc2.Card(1)
println(card3.pay())
println((bk.total, acc.bal, acc2.bal))
90
80
75
74
(74,25,19)
callbk.totalacc.balacc2.bal
start10050-
card.pay()9040-
card.pay()8030-
card2.pay() (acc, limit 5)7525-
card3.pay() (acc2, limit 1)742519

card and card2 share acc.bal. acc2 has its own bal, but all accounts share bk.total.

Try it: why is val old = run needed?

Explain without running: what would pc.run(1) do after one attach() written as run = x => step(run(x))?

The lambda mentions the field run, and a captured variable (here: a field reached through the outer object) is read when the lambda runs. At that moment run is the lambda itself, so run(1) calls step(run(1)), which calls run(1) again, without end. We ran it and caught the error: StackOverflowError. Saving the old function in a val first is what makes the composition work.

Additional argument lists

A method can have more than one pair of brackets with parameters. Each pair is an argument list (or parameter list). You call it with the same groups:

def plus(x: Int, y: Int): Int = x + y        // one list with two parameters: plus(2, 3)
def cplus(x: Int)(y: Int): Int = x + y       // two lists with one each:      cplus(2)(3)
def plus3(x: Int)(y: Int)(z: Int): Int = x + 10 * y + 100 * z
println(plus3(1)(2)(3))
321

The grouping is strict. The compiler rejects the other shape:

  • plus(2)(3): not enough arguments for method plus: (x: Int, y: Int): Int. Unspecified value parameter y.
  • cplus(2, 3): too many arguments (found 2, expected 1) for method cplus: (x: Int)(y: Int): Int

Why bother? The slides give three reasons. (1) It is the curried form (next topics). (2) It makes partial application easy. (3) It helps type inference: Scala works out the type parameters list by list, so a lambda in a later list already knows its parameter type.

def applyTo[A](a: A)(f: A => A): A = f(a)
println(applyTo(3)(x => x + 1))           // A = Int is known before the lambda is checked
println(applyTo("ab") { s => s + s })     // a last argument list can use braces
4
abab

With one list, def applyTo2[A](a: A, f: A => A), the call applyTo2(3, x => x + 1) fails with missing parameter type: A is not known yet when the lambda is checked.

The rule The brackets in the call must match the brackets in the definition: f(a, b) for one list of two, f(a)(b) for two lists. A method with additional argument lists is a curried method.
Trap Treating commas and )( as interchangeable. They are not: b(f, x) and b(f)(x) fit different headers.
On the exam Additional argument lists are everywhere in the higher-order questions (iterate(f, 3)(a), andThen(f, g)(x)). Note that you can pass such a method with its last list missing where a function is expected: andThen(f, g) with def andThen[A,B,C](f: A => B, g: B => C)(x: A): C is the function x => g(f(x)).
Try it: which calls compile?

With def b1: (Int => Int) => Int => Int, def b2(f: Int => Int, x: Int): Int, def b3(f: Int => Int)(x: Int): Int, def b4: (Int => Int, Int) => Int: which accept b(_ * 2)(5) and which b(_ * 2, 5)?

b(_ * 2)(5): b1 and b3. b(_ * 2, 5): b2 and b4. We ran the good calls (all give 10) and the wrong ones: b1(_ * 2, 5) gives too many arguments (found 2, expected 1) for method apply ... in trait Function1; b2(_ * 2)(5) gives Unspecified value parameter x; b4(_ * 2)(5) gives Unspecified value parameter v2.

Try it: why does the first compile and the second not?
def applyTo[A](a: A)(f: A => A): A = f(a)
def applyTo2[A](a: A, f: A => A): A = f(a)
applyTo(3)(x => x + 1)
applyTo2(3, x => x + 1)

In applyTo, the first list fixes A = Int, then the lambda is checked with x: Int. In applyTo2 both arguments are checked together and x has no known type: error: missing parameter type. Fix: applyTo2(3, (x: Int) => x + 1).

Partially applied functions

Partial application means: give a function only some of its arguments, and get back a function that waits for the rest. The slides: cplus(2) is "a function with one argument filled in", equivalent to (y: Int) => 2 + y. The result is called a partially applied function.

def plus(x: Int, y: Int): Int = x + y
def cplus(x: Int)(y: Int): Int = x + y
def rplus(x: Int): Int => Int = y => x + y
val vplus: Int => Int => Int = x => y => x + y

val add2a: Int => Int = cplus(2)    // curried method + expected type
val add2b = cplus(2) _               // curried method + explicit _
val add2c = rplus(2)                 // method that returns a function: no _ needed
val add2d = vplus(2)                 // function value: no _ needed
val add2e = plus(2, _)               // non-curried: placeholder for the missing argument
println((add2a(10), add2b(10), add2c(10), add2d(10), add2e(10)))
val both = plus(_, _)
println(both(4, 5))
(12,12,12,12,12)
9

Note: val e1 = cplus(2) without type and without _ is refused in Scala 2.13: missing argument list for method cplus. With rplus(2) it is fine, because rplus(2) is a complete call that simply returns a function.

With three lists you can stop after any of them:

def plus3(x: Int)(y: Int)(z: Int): Int = x + 10 * y + 100 * z
val p1: Int => Int => Int = plus3(1)
val p12: Int => Int = plus3(1)(2)
println((p1(2)(3), p12(3)))
(321,321)

When is the given argument evaluated?

A subtle point about side effects. With cplus(next()) _, Scala evaluates next() once, when the partially applied function is made. With the placeholder form plus(next(), _), the whole call (including next()) is inside the lambda, so it runs on every call.

var n = 0
def next(): Int = { n += 1; n * 100 }
val g1 = cplus(next()) _
println((n, g1(1), g1(1), n))
val g2 = plus(next(), _)
println((n, g2(1), g2(1), n))
(1,101,101,1)
(1,201,301,3)
The rule Supplying the first argument list(s) of a curried function gives a new function for the remaining ones. For a non-curried method, write _ for the missing arguments: plus(2, _) is y => plus(2, y).
Trap Calling f(3) "a nested function" in a statement question. A nested function is a def inside a def; f(3) for a curried f is a partially applied function.
On the exam Partial application appears when a curried helper is passed without its last list (iterate(f, 3), reUntil(f, c)), and in "can this header be called with ..." questions. Ask: after the arguments given so far, what is left? That leftover is the function type you hold.
Try it: what type does each expression have?

With def plus3(x: Int)(y: Int)(z: Int): Int: (a) plus3(1)(2)(3) (b) plus3(1)(2) _ (c) plus3(1) _

(a) Int, value 321. (b) Int => Int. (c) Int => (Int => Int), written Int => Int => Int. The run above shows p1(2)(3) and p12(3) both give 321.

Try it: how many times does next() run?

In the program above, why is the last n equal to 3?

g1 ran next() once at creation (n = 1); calling g1 twice ran nothing new. g2 = plus(next(), _) is y => plus(next(), y), so nothing runs at creation, and each of the two calls runs next(): n = 2 (gives 200 + 1), then n = 3 (300 + 1).

Currying and reading function types

A function of two arguments normally takes them together: plus: (Int, Int) => Int, "give me a pair of Ints, I give you an Int". The curried version takes them one at a time: cplus: Int => (Int => Int), "give me an Int, and I will give you a function from Int to Int". You call it as cplus(2)(3) instead of plus(2, 3). Every function of two arguments can be curried.

The arrow is right associative

The slides: "(=>) is right associative". That means missing brackets are always filled in from the right:

writtenmeanstakes first
Int => Int => IntInt => (Int => Int)an Int
Int => Int => Int => IntInt => (Int => (Int => Int))an Int (three in total)
(Int => Int) => Int => Int(Int => Int) => (Int => Int)a function
Int => (Int => Int) => IntInt => ((Int => Int) => Int)an Int, then a function
((Int => Int) => Int) => Intas writtena function that itself takes a function
(Int, Int) => Intas written: NOT curriedtwo Ints at once

Paper method: peel off the top-level arrow. Find the first => that is not inside brackets. Left of it is the first argument (group); right of it is what you get back. Repeat on the right part. Each peel is one pair of call brackets.

Example: ((Int, Int) => Int) => Int => Int. Peel 1: argument (Int, Int) => Int, a two-argument function like _ + _. Peel 2: argument Int. Result Int. So the call shape is b(_ + _)(2).

Moving arguments across the = sign

The slides show that you can move each argument from the left of = (as a parameter) to the right (as a lambda), and the result is equivalent. All of these have the curried type (Int => Int) => Int => Int and are all called as h(f)(x):

def h1: (Int => Int) => Int => Int = f => x => f(f(x))   // both arguments on the right
def h2(f: Int => Int): Int => Int = x => f(f(x))           // f on the left, x on the right
def h3(f: Int => Int)(x: Int): Int = f(f(x))               // both on the left, two lists
val asType: List[(Int => Int) => Int => Int] = List(h1, h2, h3)
println(asType.map(h => h(_ + 1)(0)))
println((h1(_ * 3)(1), h2(_ * 3)(1), h3(_ * 3)(1)))
List(2, 2, 2)
(9,9,9)

Not equivalent: def h(f: Int => Int, x: Int): Int (one list: called h(f, x)) and def h: Int => Int => Int => Int (first argument is an Int, not a function).

The same works when the first group has two parameters. def b: (Int => Int, Int) => Int => Int peels to "first (f, x) together, then a", so its equivalents are def b(f: Int => Int, x: Int): Int => Int and def b(f: Int => Int, x: Int)(a: Int): Int, both called b(f, x)(a). Splitting f and x into separate lists changes the grouping and is not equivalent.

Which functions are "curried"?

definitioncurried?
def t1: (Int, Int) => Int = _ * _no: one tuple-like group
def t2(x: Int)(y: Int): Intyes: additional argument list
def t3 = (x: Int) => (y: Int) => x * yyes: returns a function
def t4(x: Int): Int => Int = y => x * yyes: returns a function
def t5(x: Int, y: Int): Intno
def t6 = (x: Int, y: Int) => x * yno
The rule A => B => C is A => (B => C). A curried function takes one argument (group) per call bracket. Two headers are equivalent when they have the same type after peeling: same groups, same order, same types in each group, no matter whether a group is written as a parameter list or as a lambda after =.
Trap (1) Counting (String, String) => String as curried because two Strings appear: the brackets make it one group. (2) Thinking only f(x)(y) syntax is curried: a def f(x): Int => Int is just as curried. (3) Counting arrows without respecting brackets: (Int => Int) => Int => Int has three arrows but takes two arguments, the first being a function. (4) Thinking extra brackets on the right change anything: Int => ((Int => Int) => Int) is the same as Int => (Int => Int) => Int.
On the exam Three question shapes: "which headers are equivalent to def b: ... = f => x =>", "which headers can be called with b(_ + _)(2)", and "which functions are curried". For each option, peel it into groups and compare with the target. (The exam treats def b(f)(x) and def b: F => X => R as interchangeable; the only difference is that the method form needs an expected type or _ before you can use b(f) on its own.)
Try it: equivalent headers

Target: def q: String => (Int => String) => String = s => f => .... Which are equivalent? (a) def q(s: String)(f: Int => String): String (b) def q(s: String): (Int => String) => String = f => ... (c) def q(s: String, f: Int => String): String (d) def q: String => Int => String => String

Peel the target: group 1 String, group 2 Int => String, result String. (a) and (b) match. (c) puts both in one group. (d) peels to String, Int, String: three groups and the second is an Int. We ran (a), (b) and the target with q("ab")(n => "#" * n):

(ab##,ab##,ab##)

and assigning (c) to the target type fails: type mismatch; found: (String, Int => String) => String required: String => ((Int => String) => String).

Try it: which header fits k(_ + _)(4)?
def k1: ((Int, Int) => Int) => Int => Int = f => x => f(x, x)
def k2(f: (Int, Int) => Int): Int => Int = x => f(x, x)
println((k1(_ + _)(4), k2(_ * _)(4)))
(8,16)

Both fit: first group is a two-argument function, second group is an Int. A header starting with Int => would need a number first; def k(f: (Int,Int) => Int, x: Int) would need k(_ + _, 4).

Try it: read the type

How many arguments does Int => (Int => Int) => Int take, and of which types? And (Int => Int => Int) => Int?

First: two. An Int, then a function Int => Int; result Int. Call shape b(3)(x => x + 1). Second: one argument, which is a curried function Int => Int => Int; result Int. Call shape b(x => y => x + y).

Converting: curry and uncurry

You can write higher order functions that convert between the two forms. Look at their types, because the types decide what you may pass in:

def curry[A,B,C](f: (A, B) => C): A => B => C = a => b => f(a, b)     // in: uncurried, out: curried
def uncurry[A,B,C](f: A => B => C): (A, B) => C = (a, b) => f(a)(b)  // in: curried,   out: uncurried

curry only accepts a two-argument function; uncurry only accepts a curried one. A method is converted to the right function value automatically (eta-expansion), so curry(plus) and uncurry(rplus) and even uncurry(cplus) work.

val tl: (Int, Int) => Int = _ - _
val cl: Int => Int => Int = curry(tl)
println((cl(10)(3), uncurry(cl)(10, 3), curry(plus)(10)(3), uncurry(rplus)(10, 3), curry(uncurry(cl))(10)(3)))
val u2 = uncurry(cplus)
println(u2(1, 2))
(7,7,13,13,7)
3

Passing the wrong form does not compile. curry(cl) gives type mismatch; found: Int => (Int => Int) required: (?, ?) => ?.

Curry and uncurry are each other's inverse: curry(uncurry(f)) behaves like f for a curried f, and uncurry(curry(g)) behaves like g for a two-argument g. The slides call these "complicated identity functions".

You will see curry written in many equivalent ways, and they are all curry: def p[A,B,C](f: (A,B) => C)(a: A)(b: B): C = f(a, b); def p[A,B,C]: ((A,B) => C) => A => B => C = f => a => b => f(a, b); def p[A,B,C](f: (A,B) => C)(a: A): B => C = b => f(a, b); or a local def q(a: A)(b: B) = f(a, b) returned as q. The test is the shape: the input is a two-argument function, after it the arguments come one per bracket. If instead the input is A => B => C and the output takes (x, y) together, it is uncurry.

(The Scala library has these too: add3.curried on a function value, and Function.uncurried.)

Checking a claimed equivalence X :=: Y

  1. Work out the type of each side. If one is curried and the other is not, they are not equivalent (unless a conversion is applied).
  2. If a side passes the wrong form to curry/uncurry, it does not even compile, so the claim is false.
  3. If types match, check behaviour on one example: both sides with arguments 10 and 3.
The rule curry: ((A,B) => C) => A => B => C and uncurry: (A => B => C) => (A,B) => C. Feed each only its own input form. They undo each other.
Trap curry(cm) where cm is already curried, uncurry(tl) where tl already takes a pair: type errors, so false. uncurry(acp) :=: cp compares an uncurried function with a curried one: false. Also, a curry or uncurry with extra argument lists (def uncurry(f: A => B => C)(x: A, y: B): C) is the same function, just written with the arguments on the left.
On the exam "Which of the following equivalences are true?" with three versions of plus (pair, two lists, returns lambda) plus a curry and an uncurry. Make a small table: each name with its form (curried or not). Then each claim takes ten seconds. "What is the name of this higher order function?" with (f: (A,B) => C)(x: A)(y: B): C = f(x, y): curry.
Try it: true or false?

With def ncp(x: Int, y: Int), def acp(x: Int)(y: Int), def cp(x: Int): Int => Int: (a) cp :=: acp (b) curry(ncp) :=: acp (c) uncurry(acp) :=: cp (d) curry(uncurry(acp)) :=: cp (e) curry(acp) :=: ncp

(a) true: both curried, same result. (b) true. (c) false: the left is uncurried, the right curried. (d) true: round trip. (e) false: curry needs an uncurried input, so this is a type error. Our run of the same shapes printed (7,7,13,13,7) and 3 for the legal ones, and curry(cl) failed to compile.

Try it: is this curry or uncurry?

def f3[A,B,C](f: A => B => C)(x: A, y: B): C = f(x)(y)

Uncurry: its input is curried, and the remaining arguments come together as (x, y). Do not decide by counting argument lists: it has two lists, but that is just the "arguments on the left" way of writing (A, B) => C as the result.

Laziness: def, val, lazy val

Eager evaluation: an expression is computed as soon as it is bound to a name. Lazy evaluation: it is computed only when it is needed. Scala is eager by default, but lets you choose per definition. The slides:

definitionwhen is the body run?how often?
val v = {...}at the declaration (eager)exactly once, even if never used
lazy val l = {...}at the first useat most once; later uses reuse the cached value
def d = {...}at every useonce per use; zero if never used

The body is usually a block with a print in it, so the printed letters tell you exactly when it ran. Remember: a block's value is its last expression, so {print("v"); 3} prints v and has value 3.

Paper method: a timeline. Go through the code line by line. At each declaration: val prints now; lazy val and def print nothing. At each line that uses names, go through the expression from left to right: every def use prints; the first lazy val use prints and you mark it "done"; later uses print nothing. Keep a column with running values.

def run1(): Unit = {
  def d: Int = { print("D"); 1 }
  val v: Int = { print("V"); 2 }
  lazy val l: Int = { print("L"); 3 }
  print("|")
  var s = l
  s += d
  s += l
  s += v
  s += d
  println("| s=" + s)
}
run1()
V|LDD| s=10
lineprintsswhy
def ddef: nothing yet
val vVval: runs at declaration
lazy val llazy: waits
print("|")|
var s = lL3first use of l
s += dD4def runs every time
s += l7cached
s += v9already computed
s += dD10def runs again

A lazy val that is only used in a branch that does not run is never computed at all:

def run2(): Unit = {
  lazy val l: Int = { print("L"); 3 }
  def d: Int = { print("D"); 1 }
  val v: Int = { print("V"); 2 }
  var s = 0
  if (v > 5) { s += l; s += l } else { s += d; s += d }
  println("| s=" + s)
}
run2()
VDD| s=2

Counters: values depend on the moment of evaluation

When the body bumps a shared counter, the value you get tells you when it ran. Expressions are evaluated left to right, and an eager val has already bumped the counter before the line that uses it.

var n = 0
def bump(): Int = { n += 1; n }
def run4(): Unit = {
  lazy val c = bump()
  def b = bump()
  val a = bump()                    // runs now: n = 1, a = 1
  val r = (c + c, b + b, a + a, n)
  println(r)
}
run4()
(4,7,2,4)

c + c: first use, bump gives 2, the second c reuses 2: 4. b + b: two runs, 3 + 4 = 7. a + a = 1 + 1 = 2. Finally n is 4.

Inside a class: one cache per object

A val field runs when the object is created (in the constructor), even if nobody reads it. A lazy val field is computed once per object, at that object's first use. A def (or a method p()) runs at every call.

class Report(val id: Int) {
  val made: Int = { print(s"made$id "); id }
  lazy val summary: Int = { print(s"sum$id "); id * 10 }
  def fresh: Int = { print(s"fresh$id "); id + 1 }
}
val r1 = new Report(1)
val r2 = new Report(2)
println("|")
println(r1.summary + r1.summary + r2.fresh + r2.fresh + r2.summary)
made1 made2 |
sum1 fresh2 fresh2 sum2 46

r2.summary prints again because r2 is a different object with its own cache. Sum: 10 + 10 + 3 + 3 + 20 = 46.

Same mechanism for parameters: by-name (=> Int)

The slides do not use this term, but it is the same idea and helps when you trace side effects. A normal parameter x: Int is by-value: the argument is computed once, before the call, like a val. A by-name parameter x: => Int is computed every time the body uses it, like a def. Storing it in a lazy val inside makes it "at most once".

def tick(tag: String): Int = { print(tag); 1 }
def byValue(x: Int): Int = x + x
def byName(x: => Int): Int = x + x
def cached(x: => Int): Int = { lazy val y = x; y + y + y }
def maybe(c: Boolean, x: => Int): Int = if (c) x else 0
println(" " + byValue(tick("v")))
println(" " + byName(tick("n")))
println(" " + cached(tick("c")))
println(maybe(false, tick("m")))
println(maybe(true, tick("M")))
v 2
nn 2
c 3
0
M1

This is also how a "control structure" with an additional argument list works: def repeat(k: Int)(body: => Unit), called as repeat(3) { print("hi ") }, prints hi hi hi .

Why laziness? (slides) It does not compute what is not needed, can save work, and allows infinite data structures. The price: the evaluation order is hard to follow, which is why lazy languages combine it with pure functions only.

The rule val: now, once. lazy val: at first use, once (per object, for fields). def: at every use. In an expression, uses are evaluated left to right.
Trap (1) Printing the lazy val at its declaration (it waits for its first use). (2) Caching a def or a method p() (it runs every time). (3) Printing a lazy val twice (it is cached after the first use). (4) Forgetting that a val declared later in the code still prints before any use of an earlier lazy val or def. (5) Thinking a lazy field is cached once globally: each object has its own. (6) Re-printing a on x += a: a val is just read.
On the exam Every paper has one: "What does this print?" with a lazy val, a def and a val whose bodies print a letter or bump a counter, followed by s += ... lines or a tuple. The order of declarations is shuffled between versions on purpose. Use the timeline table; write the letters in order, and the counter value after every evaluation.
Try it: print order
def t1(): Unit = {
  val x: Int = { print("x"); 1 }
  lazy val y: Int = { print("y"); 2 }
  def z: Int = { print("z"); 3 }
  var s = z
  s += x
  s += y
  s += z
  s += y
  println(" s=" + s)
}
t1()
xzyz s=11

x at declaration. s = z: z (3). += x: nothing (4). += y: y, first use (6). += z: z again (9). += y: cached (11).

Try it: a branch decided by a def
def t2(): Unit = {
  lazy val y: Int = { print("y"); 2 }
  def z: Boolean = { print("z"); false }
  val x: Int = { print("x"); 1 }
  var s = 0
  if (z) { s += y } else { s += x; s += x }
  println(" s=" + s)
}
t2()
xz s=2

Although y and z are declared first, only the val x runs at declaration. Then the if runs z once, gets false, and the else branch never touches y, so "y" is never printed.

Try it: a counter in a tuple
var m = 0
def nxt(): Int = { m += 1; m }
def t3(): Unit = {
  def a = nxt()
  lazy val b = nxt()
  val c = nxt()
  println((b + a, b + a, c, m))
}
t3()
(5,6,1,4)

c = 1 at declaration. First element: b forced (2) + a (3) = 5. Second: b cached (2) + a (4) = 6. Then c = 1 and m = 4.

Try it: lazy fields on two objects
class Cfg(val k: Int) {
  lazy val big: Int = { print("B" + k + " "); k * 100 }
  def small: Int = { print("s" + k + " "); k }
}
val q1 = new Cfg(1); val q2 = new Cfg(2)
var acc = 0
acc += q1.small; acc += q1.big; acc += q2.big; acc += q1.big; acc += q2.small
println(acc)
s1 B1 B2 s2 403

Nothing prints at construction (no eager val with a print). q1.big prints once, q2.big prints once for its own object, the second q1.big is cached. 1 + 100 + 200 + 100 + 2 = 403.

Recap: Week 3 in one screen

topicrememberfirst move on the exam
Local functionssee and change outer locals; nearest declaration wins; a new var or a parameter shadowsdraw an arrow from each name to its declaration
First class functionsfunctions are values: variable, argument, result, field; types A => B; method to function needs an expected type or _functions as data: ask it about one x, never loop over it
Lambdas(x: Int) => body; body runs per call; locals inside are fresh every callis the var inside or outside the arrow?
Higher order functionsx => g(f(x)) runs f first; read bodies, not namesflatten to a left to right pipeline, push the number through
Placeholderseach _ is a new parameter; f(g(_)) is f(x => g(x))count the underscores against the expected arity
Closurescapture the variable, not the value; var read at call time; one new variable per maker call; assignment sharesbox per maker call, arrows for variables, table per call
Nested classesinner object tied to its outer object; own inner fields, shared outer fields; a.B path dependentbox per new, "outer" arrows, update the box the arrow leads to
Additional argument listsf(a)(b) vs f(a, b) are not interchangeable; helps type inferencematch brackets of call and definition
Partial applicationcplus(2), cplus(2) _, plus(2, _); argument of m(e) _ computed oncewhat is left after the given groups?
CurryingA => B => C = A => (B => C); (A, B) => C is not curried; moving params across = keeps equivalencepeel the top-level arrow, compare groups
Uncurryingcurry: ((A,B) => C) => A => B => C, uncurry the reverse; wrong input form = type errorlabel each function curried or not
Lazinessval now and once; lazy val first use and once (per object); def every use; by-name like deftimeline table of prints and counter values

Three questions that solve most of this week: (1) Which declaration does this name refer to? (2) When does this code actually run: at declaration, at first use, or at every use/call? (3) Is this a new thing (new var, new object, new closure) or a second name for an existing one?

Week 4: Polymorphism (generics, subtyping, inheritance, overloading)

This block is about one question: when your code says x.m(y), which piece of code actually runs? Scala answers it in two moments: the compiler decides some things from the static types it can see, and the running program decides the rest from the actual object. About a quarter of every past exam (dispatch traces, overloading, "which errors are reported", "most specific type", iterators) is this one idea in different clothes.

Polymorphism: one piece of code, many types

Polymorphism is Greek for "many forms". Polymorphic code is code that works for more than one type. The goal is reuse: write speakTwice once and have it work for dogs, cats and parrots.

First some words you need for the whole block.

  • A class is a blueprint (class Dog). An object (or instance) is one thing built from it with new Dog.
  • A variable such as val a: Animal = new Dog does not contain the dog itself. It holds a reference: an arrow pointing to the dog object somewhere in memory.
  • The static type of an expression is the type the compiler knows, from declarations alone, without running anything. Here the static type of a is Animal, because that is what you wrote.
  • The dynamic type (or run-time class) is the class of the object the arrow really points to. Here it is Dog. It is only known when the program runs, and it can change if a var is reassigned.
  • D is a subtype of A (written D <: A) means: a D can be used anywhere an A is expected. class Dog extends Animal makes Dog <: Animal.

The professor names three kinds of polymorphism:

KindIdeaExampleChosen when?
Parametric (generics)The same code for all typesdef swap[A,B](p: (A,B)): (B,A)Compile time
SubtypeDifferent code per (sub)type, behind one common supertypeanimal.speak runs Dog's or Cat's speakRun time (dynamic dispatch)
Ad hoc (overloading)Several methods with the same name and different parameter typesmax(l: List[Int]) and max(a: Int, b: Int)Compile time, from static types
class Animal { def speak: String = "..." }
class Dog extends Animal { override def speak = "woof" }
class Cat extends Animal { override def speak = "miauw" }

var pet: Animal = new Dog
println(pet.speak)
pet = new Cat
println(pet.speak)
woof
miauw

The static type of pet is Animal on every line: a declaration never changes. The dynamic type is Dog and then Cat. The output follows the dynamic type.

The rule Keep two columns for every expression: the static type (what the compiler sees, fixed by declarations) and the dynamic type (the class of the real object). The compiler only ever uses the first column. The run-time choice of an overridden method uses the second.
The two-column method (use it for every trace in this block)
  1. For each variable or expression, write static type | dynamic class. Static comes from the declaration (val x: B, a parameter type, a method's declared result type, a list's element type). Dynamic comes from the new that created the object.
  2. Compile time, static column: does the call type-check, and if the name is overloaded, which signature is picked?
  3. Run time, dynamic column: for that signature, start at the object's class and walk up the parents until you find a body. That body runs.
  4. Inside the body, this is the same object, so plain calls like y (meaning this.y) go back to step 3 with the same dynamic class.
Trap A heterogeneous list has a boring static element type. val l = List(1, "two") has type List[Any], so l(0) has static type Any, even though the object really is an Int. val n: Int = l(0) does not compile:
error: type mismatch;
 found   : Any
 required: Int
The compiler never peeks at the value to refine a static type.
On the exam Multiple-response questions ask "which languages support inheritance" (C#, Java, C++, Scala yes; C no) and "which support parametric polymorphism / generics" (Java, C#, Scala yes; C and Python no, according to the course: C only has void* tricks and Python is dynamically typed, so it has no type parameters checked by a compiler).
Try it: which line does not compile, and why?
val things = List(true, "yes", 7)
val first = things(0)
val ok: Boolean = first

Answer. things is List[Any] (the only common supertype of Boolean, String and Int is Any). So first has static type Any, and line 3 fails with "type mismatch; found: Any, required: Boolean". The fact that the value is true at run time does not matter: types are checked before anything runs.

Try it: static and dynamic type, line by line
var pet: Animal = new Cat   // line 1
pet = new Dog               // line 2
val d: Dog = new Dog        // line 3

Answer. Line 1: pet is Animal | Cat. Line 2: Animal | Dog (the static type stays Animal, only the arrow moved). Line 3: d is Dog | Dog. If you then wrote pet = new Cat again, it compiles; val d2: Dog = pet would not, because the compiler only knows pet is an Animal.

Generics (parametric polymorphism)

A type parameter is a placeholder for a type, written in square brackets. You use it when the code does not care what the type is, only that it is the same type in several places.

Generic functions

def swap[A, B](p: (A, B)): (B, A) = (p._2, p._1)
println(swap((42, "hello")))      // A = Int, B = String, result (String, Int)
(hello,42)

You almost never write swap[Int, String](...): the compiler infers A and B from the static types of the arguments. The signature then promises the result type: (B, A) becomes (String, Int).

Generic classes

class Stack[A] {
  private var elements: List[A] = Nil
  def push(x: A): Unit = elements = x :: elements
  def pop(): A = { val top = elements.head; elements = elements.tail; top }
}
val st = new Stack[String]
st.push("a"); st.push("b"); st.push("c")
print(st.pop()); print(st.pop()); println(st.pop())
cba

new Stack[String] "instantiates A with String": now push only accepts Strings and pop() has static type String.

The type of a call: read the signature, not the value

This is how "what is the (most specific) type of v" questions work. The static type of a call f(arg) is the declared result type of f, with any type parameters replaced by what was inferred from the argument's static type. What the body does at run time is irrelevant. Work inside out.

abstract class Animal { def speak: String }
class Dog extends Animal { def speak = "woof" }
class Cat extends Animal { def speak = "miauw" }
class Puppy extends Dog { override def speak = "yip" }

def anyId(x: Any): Any = x
def id[A](x: A): A = x
def animalId(x: Animal): Animal = x
def boundId[A <: Animal](x: A): A = x
def boundToAnimal[A <: Animal](x: A): Animal = x

(anyId(new Dog), id(new Cat), animalId(new Dog), boundId(new Puppy), boundToAnimal(new Cat))
(Any, Cat, Animal, Puppy, Animal)

(That line is the static type, printed with a TypeTag.) Only the functions whose result type is the type parameter (id, boundId) keep the precise type. A result type written as a concrete class (Any, Animal) forgets it, even if the function is generic.

Upper type bounds: A <: Animal

Suppose you want to return an animal together with its sound, without forgetting what kind of animal it was.

def withSound(a: Animal): (Animal, String) = (a, a.speak)      // forgets: result is Animal
def withSound[A](a: A): (A, String) = (a, a.speak)              // does not compile
def withSound[A <: Animal](a: A): (A, String) = (a, a.speak)   // keeps A and may call speak

The middle one fails with error: value speak is not a member of type parameter A: for an arbitrary A the compiler cannot know there is a speak. The bound A <: Animal means "A can be any subtype of Animal". That gives permission to call Animal methods, and the result is still typed as the precise A: withSound(new Puppy) has type (Puppy, String) and value (..., "yip").

Generic case classes and their fields

case class Cage(a: Animal)
case class GCage[A <: Animal](a: A)
(Cage(new Dog).a, GCage(new Dog).a)
(Animal, Dog)

Cage's field is declared Animal, so reading it gives Animal. GCage(new Dog) infers A = Dog, so its field has type Dog.

Generics plus inheritance: Comparable[A]

The slides combine both kinds of polymorphism. A generic trait gives default methods, and each class says "I am comparable to myself" by extending it with its own name:

trait Comparable[A] {
  def compareTo(rhs: A): Int                         // < 0, 0 or > 0
  def <(rhs: A): Boolean = compareTo(rhs) < 0        // inherited for free
}
case class Money(cents: Int) extends Comparable[Money] {
  def compareTo(rhs: Money): Int = cents - rhs.cents
}
case class Name(s: String) extends Comparable[Name] {
  def compareTo(rhs: Name): Int = s.length - rhs.s.length
}
case class Pair[A <: Comparable[A], B <: Comparable[B]](a: A, b: B)
    extends Comparable[Pair[A, B]] {
  def compareTo(rhs: Pair[A, B]): Int = {
    val c = a.compareTo(rhs.a)
    if (c != 0) c else b.compareTo(rhs.b)        // second field only breaks a tie
  }
}

A and B are separate parameters, so a pair of a Money and a Name is fine. The bound A <: Comparable[A] is what allows a.compareTo(rhs.a). The same pattern appears as def sort[A <: Ordered[A]](arr: Array[A]): "works for any A that has the Ordered[A] trait", so inside you may write arr(i) > arr(i+1).

The rule The static type of f(e) is f's declared result type with its type parameters filled in from the static type of e. A bound A <: T is a restriction on what A may be (and a permission to use T's methods), never the resulting type.
Trap Three classic slips: (1) using the run-time class: animalId(new Dog) is Animal, not Dog; (2) widening to the bound: boundId(new Puppy) is Puppy, not Animal; (3) forgetting that a non-generic function in the middle of a chain forgets the type for good: id(animalId(new Dog)) is Animal, because id only sees an Animal coming in.
Trap (subtyping of generic classes, details in week 6) A plain class Box[A] is invariant: Box[Dog] is not a Box[Animal] and not the other way round, even though Dog is an Animal. class Box[+A] is covariant (Box[Dog] <: Box[Animal], like List), and class Printer[-A] is contravariant (Printer[Animal] <: Printer[Dog]: something that can print any animal can print a dog).
On the exam "Most specific type of m" questions: write each sub-expression with its static type, inside out. For each call, ask only "what does the signature say it returns?". For a generic case class, ask "is the field typed A or a fixed class?". Tuples: do each component separately. Comparable-tuple questions: compute the first component's compareTo; only if it is 0 look at the second; keep the subtraction order exactly as written (n * rhs.d - rhs.n * d).
Try it: static type of a chain
def makeDog(x: Animal): Dog = new Puppy
val v = boundId(id(animalId(makeDog(new Cat))))
val w = id(makeDog(new Cat))

Answer. v: Animal, w: Dog (both checked with the compiler). Inside out for v: makeDog(...) is declared Dog (the Cat argument and the Puppy inside do not matter); animalId(...) is declared Animal; id receives an Animal so A = Animal; boundId receives an Animal so A = Animal. For w: id receives a Dog, so the result is Dog, not Puppy.

Try it: the type follows the variable, not the object
val pup = new Puppy
val asDog: Dog = pup
val t = (id(asDog), boundId(asDog), id(pup))

Answer. (Dog, Dog, Puppy). asDog and pup point to the same object, but their static types differ, and type inference only uses static types.

Try it: compareTo on generic pairs
Pair(Money(500), Name("Ann")).compareTo(Pair(Money(500), Name("Bob")))
Pair(Money(500), Name("Anna")).compareTo(Pair(Money(500), Name("Bob")))
Pair(Money(300), Name("Anna")).compareTo(Pair(Money(500), Name("Bob")))
Pair(Money(1), Name("xyz")) < Pair(Money(2), Name("y"))
0
1
-200
true

Answer. 1: money 500 - 500 = 0, so compare names: 3 - 3 = 0. 2: money tie, names 4 - 3 = 1. 3: money 300 - 500 = -200, not 0, so the names are never looked at. 4: < is inherited from Comparable and calls compareTo, which gives 1 - 2 = -1, and -1 < 0 is true.

Subtype polymorphism

Imagine you only had Any. A function that makes any animal speak would have to test the type of its argument:

def speak(animal: Any): String = animal match {
  case _: Dog    => "Woof woof!"
  case _: Cat    => "Miauw!!"
  case a: Parrot => a.line
  case _         => "<Silence>"
}

The slides list the downsides: users of your library cannot add a new animal without editing this function, and Any is far too loose a type. The fix is a common supertype that promises a speak method, with each subclass giving its own implementation:

abstract class Animal { def speak: String }          // a promise, no body
class Dog extends Animal { def speak = "Woof woof!" }
class Cat extends Animal { def speak = "Miauw!!" }
class Parrot(val line: String) extends Animal { def speak = line }

def speakTwice(animal: Animal): String = animal.speak + animal.speak

Now speakTwice compiles because every Animal is guaranteed to have speak, and it works for animals that do not exist yet. Which speak runs is decided per object, at run time.

What "subtype" means

D is a subtype of A (D <: A) means: D supports at least the interface of A, so a D can be used anywhere an A is expected. That is the substitution principle. Consequences:

  • def f(x: A): B accepts any value whose type is a subtype of A, and may return any subtype of B.
  • var v: A can hold any value of a subtype of A. A variable may have a trait type: var x: X = new B is fine when B extends trait X.
  • The direction matters: var d: Dog = new Animal is an error (type mismatch; found: Animal, required: Dog). Not every animal is a dog.
  • Every type is a subtype of itself, and of Any.
  • Subtype is not the same as subclass: a subclass is a subtype, but traits and function types also create subtypes.

One value, many types

Because of substitution, a value has many types at once. A new Puppy is a Puppy, a Dog, an Animal, an AnyRef and an Any. A list of several objects therefore has every type that is a common supertype of all elements; the compiler infers the lowest one (the professor calls it the lub, least upper bound: the lowest common ancestor in the hierarchy diagram).

trait K; trait M; trait N
trait S extends K with M
class T extends S
class U extends M with N
class V extends S with N

List(new T, new U, new V)     // inferred type
List(new T, new V)
List[M]
List[S]

Method: draw the diagram, then for each element write the set of all its supertypes. T: {T, S, K, M}. U: {U, M, N}. V: {V, S, K, M, N}. The intersection is {M} (plus AnyRef and Any, which everyone has). So the list "has" the types List[M], List[AnyRef], List[Any], and as a value it is also simply Any. It does not have List[S] (U is not an S) or List[N] (T is not an N). The same reasoning answers "which return types can I write for a function that returns new A in one branch and new B in another".

The rule A value of type D can go wherever an A is expected exactly when D <: A. A variable, parameter or list element then has the static type you declared (or the lub the compiler inferred), and the object inside keeps its own class.
Trap In "which types does l have" questions, students pick a trait that two of the three elements have, or a class that only one element is. Check every element. And do not forget the always-true answers: List[Any] and plain Any. Nothing is at the bottom of the hierarchy, so it is never a supertype of real objects.
On the exam Two families: (1) "what does this print" with a list typed List[A] holding B's and C's: the element type only decides what compiles; the output follows each object (see Inheritance below). (2) "which types does l have / what can ??? be": draw the diagram, list each element's ancestors, intersect.
Try it: types of an if
val c: Boolean = ...
val a = if (c) new Dog else new Cat
val b = if (c) 1 else "one"

Answer. a: Animal (lowest common ancestor of Dog and Cat), b: Any (Int and String only share Any). Verified with the compiler. The static type of an if must cover both branches because the compiler does not know which one runs.

Try it: which assignments compile?
val l: List[M] = List(new T, new U, new V)
val l2: List[Any] = l
val l3: Any = l
val l4: List[AnyRef] = l
val l5: List[S] = l

Answer. The first four compile (List is covariant, so a List[M] is also a List[AnyRef] and a List[Any]). The last one does not: the compiler only knows the elements are M's, and an M is not necessarily an S.

Inheritance and dynamic dispatch

Inheritance means: a subclass automatically gets every definition of its superclass. You only write what is new or different. Writing a new body for an inherited method is overriding.

abstract class Shape {
  def sides: Int                                   // abstract: no body
  def name: String = "shape"                       // default
  def describe: String = name + " has " + sides + " sides"
}
class Triangle extends Shape { def sides = 3; override def name = "triangle" }
class Square extends Shape { def sides = 4 }
class Box extends Square { override def describe = "a box" }

val shapes: List[Shape] = List(new Triangle, new Square, new Box)
for (s <- shapes) println(s.describe)
triangle has 3 sides
shape has 4 sides
a box

describe is written once in Shape, but the name and sides it calls are filled in by each object. That is the main trick of this whole block.

Dynamic dispatch: "always use the deepest implementation"

Dispatch means choosing which body runs for a call. In Scala (and Java) every method call x.m() uses dynamic dispatch (also called late binding): at run time, look at the class of the object x points to, and use the deepest implementation of m for that class. "Deepest" means: start at the object's own class; if it defines m, use it; otherwise go to its parent, and so on up.

The crucial consequence: where the calling code is written does not matter. When Shape.describe says name, it means this.name, and this is the actual object (a Triangle). So the lookup for name starts at Triangle again.

Another way to see it: this is a hidden parameter

Think of every method as having an invisible first parameter this. t.describe is really describe(this = t). When describe's body says name, that is this.name, and this is still t, the Triangle. Being "inside Shape's code" does not turn the object into a Shape.

class T1 { def who: String = "T1"; def me: T1 = this; def hello: String = "I am " + this.who }
class T2 extends T1 { override def who = "T2" }
val t: T1 = new T2
println(t.hello + " " + t.me.who)
I am T2 T2

The trace method on a full example

class K { def f(x: Int): Int = x + 1; def g(x: Int): Int = x * 10 }
class L extends K { override def f(x: Int) = x + 100 }
class M extends L { override def g(x: Int) = x * 2 }
class N extends M
val ks: List[K] = List(new K, new L, new M, new N)
println(ks.map(k => k.f(1) + k.g(1)))

First make a method table: for each class, which body each method uses (look in the class, else take the parent's row).

classfg
KK.f (x+1)K.g (x*10)
LL.f (x+100)from K: x*10
Mfrom L: x+100M.g (x*2)
Nfrom M (= L): x+100from M: x*2

Then the two columns per element. The static type is K for all of them (it only checks that f and g exist on K). The dynamic class picks the row: K: 2 + 10 = 12. L: 101 + 10 = 111. M: 101 + 2 = 103. N: same row as M, 103.

List(12, 111, 103, 103)

Note N: a class that overrides nothing behaves exactly like its parent, not like the root class.

Methods that return objects

When a method returns a new object, its declared result type is the static type, and the class after new is the dynamic type. Evaluate chains left to right, one object at a time.

class A { def mk: A = new B; def same: A = this; def x: String = "A" }
class B extends A { override def x = "B"; override def mk: A = new A }
val a: A = new A
val b: A = new B
println(a.mk.x + b.mk.x + b.same.x + a.mk.mk.x + b.mk.same.mk.x)
BABAB
expressionsteps (dynamic class of each object)x
a.mk.xa is an A; A.mk makes a BB
b.mk.xb is a B; B overrides mk, makes an AA
b.same.xsame returns this, the B itselfB
a.mk.mk.xA makes B; that B's mk makes an AA
b.mk.same.mk.xB makes A; same is that A; A's mk makes BB

Only the receiver is dispatched on

In x.pair(o), the object before the dot (the receiver) picks which pair runs. The argument o does not. Inside the body, y means this.y (the receiver's y) and o.y is a separate dynamic call on the argument.

class U { def y: String = "u"; def pair(o: U): String = y + o.y }
class W extends U { override def y = "w" }
class Z extends W { override def pair(o: U): String = o.y + y }
val u: U = new U; val w: U = new W; val z: U = new Z
println(u.pair(w) + "," + w.pair(z) + "," + z.pair(u))
uw,ww,uw

w.pair(z): W has no pair, so U.pair runs with this = the W: y is "w", o.y is Z's y, which Z inherits from W: "w". z.pair(u): Z's own pair, with the order swapped: o.y = "u", then y = "w".

Traits in the hierarchy

Traits (week 6) take part in the same lookup. An override in a trait beats the trait it extends, and the class's own definition beats everything it inherits.

trait Loud { def noise: String = "LOUD" }
trait Soft extends Loud { override def noise = "soft"; def extra = "+" }
trait Tagged { def tag = "t" }
class Speaker extends Tagged with Soft { override def tag = "T" }
val s = new Speaker
println(s.noise + s.extra + s.tag)
soft+T

The order in which traits are listed only matters when two of them define the same method; that is the week 6 rule "deepest first, last trait first".

Construction order: fields are not ready yet

When you write new Derived, Scala first runs the superclass constructor (the body of class Base { ... }, top to bottom), and only then the subclass body. Until a val line has run, the field holds its default: 0 for numbers, false, or null for objects. Dynamic dispatch still works during construction, so a superclass constructor that calls an overridden method can see the subclass's fields uninitialised.

class Base {
  println("Base body starts")
  val greeting: String = "hi from " + label
  def label: String = "Base"
  println("Base body ends")
}
class Derived extends Base {
  println("Derived body starts")
  val n: Int = 7
  val word: String = "seven"
  override def label: String = "Derived n=" + n + " word=" + word
  println("Derived body ends")
}
val d = new Derived
println(d.greeting)
println(d.label)
Base body starts
Base body ends
Derived body starts
Derived body ends
hi from Derived n=0 word=null
Derived n=7 word=seven

While Base computes greeting, label already dispatches to Derived's version (the object is a Derived from the first moment), but n and word have not been assigned yet. Constructor parameters, in contrast, are available immediately:

class Counter(start: Int) {
  var count: Int = start
  def inc(): Unit = count += step
  def step: Int = 1
}
class Fast(start: Int) extends Counter(start * 10) { override def step: Int = 5 }
val c: Counter = new Fast(2)
c.inc(); c.inc()
println(c.count)
30

new Fast(2) passes 2 * 10 = 20 to Counter, so count starts at 20. Each inc() is Counter's code, but step dispatches to Fast: 20 + 5 + 5 = 30.

The other choice: static dispatch

Some languages choose the body at compile time from the variable's type (static dispatch). That is the default in C++ and C# unless a method is marked virtual; it is cheaper because there is no run-time lookup. Scala and Java only have dynamic dispatch. With static dispatch, val a: Animal = new Dog; a.describe would print the generic animal text; in Scala it prints the dog's. If an exam answer looks like "every call uses the declared type's code", it is describing static dispatch, which is wrong for Scala.

The override keyword

  • Replacing a method that already has a body: override is required. Without it: error: `override` modifier required to override concrete member.
  • Implementing an abstract method (no body in the parent): override is optional. That is what the slide means by "override annotation optional".
The rule For x.m(), the body is chosen at run time from the class of the object x points to: the deepest definition of m on the way from that class up to the root. Calls inside that body without a receiver are calls on this, which is still the same object, so they dispatch from the bottom again.
Trap The professor's favourite wrong answers all come from one mistake: "the code of A.v is written in A, so its call to a uses A's a". No. Example: class P1 { def a = "p"; def b = a + a }, class Q1 extends P1 { override def a = "q" }; (new Q1: P1).b is "qq", not "pp". The declared type P1 or List[P1] changes nothing.
Trap Do not apply every override "cumulatively". Each call runs exactly one body (unless that body itself uses super). If D overrides f as x * 3, then d.f(2) is 6, whatever B and C did with f.
On the exam "What does main print" with classes A, B, C: (1) build the method table (class by method, filling inherited cells from the parent row); (2) for each call, find the object's class and read its row; (3) for each body, expand calls on this by reading the same row again, and calls on another object by reading that object's row; (4) write the output string piece by piece in the order of the expression (watch a.y + y versus y + a.y). Never use the static column for which body runs.
Try it: one small change, different answer
class P1 { def a: String = "p"; def b: String = a + a }
class Q1 extends P1 { override def a = "q" }
class P2 { def a: String = "p"; def b: String = a + a }
class Q2 extends P2 { override def b = "q" }
val v1: P1 = new Q1
val v2: P2 = new Q2
println(v1.b + " " + v2.b)
qq q

Answer. v1 is P1 | Q1. Q1 has no b, so P1.b runs with this = the Q1; both a calls dispatch to Q1.a: "qq". v2 is P2 | Q2; Q2 overrides b itself, so P2.b and its calls to a never run: "q".

Try it: a three-level chain without super
class A { def x: String = "<" + y; def y: String = "-" + z; def z: String = "a" }
class B extends A { override def y: String = "=" + z }
class C extends B { override def z: String = "c" }
for (e <- List[A](new A, new B, new C)) print(e.x + " ")

Answer. Method table: A uses (A.x, A.y, A.z); B uses (A.x, B.y, A.z); C uses (A.x, B.y, C.z). A: "<" + "-" + "a". B: "<" + "=" + "a". C: "<" + "=" + "c". Each cell comes from the nearest class that defines it.

<-a <=a <=c 
Try it: who constructs what

Using the A/B classes with mk and same from above, what is b.same.mk.x + a.same.mk.x?

Answer. b.same is the B itself; B's mk makes an A; x = "A". a.same is the A; A's mk makes a B; x = "B". Result AB (verified). The declared result type A of mk says nothing about which class was really created.

Try it: constructor arguments and dispatch

With Counter and Fast from above: what is c.count after val c: Counter = new Fast(1); c.inc()? And after val c2 = new Counter(1); c2.inc()?

Answer. Fast(1) passes 10 to Counter, then one inc adds Fast's step 5: 15. A plain Counter(1) starts at 1 and adds step 1: 2.

15 2

Calling the superclass: super

Sometimes an override wants to extend the parent's behaviour instead of replacing it. super.m runs the parent's version of m:

class Dog extends Animal { def speak: String = "Woof woof!" }
class GoldenRetriever extends Dog { override def speak: String = super.speak + " old chap!" }

new GoldenRetriever().speak gives Woof woof! old chap! (verified).

The two precise facts about super

  1. super is static. super.m written inside class B always means "the m that B's parent has", decided by where the code is written, not by the object's class. That is why super chains do not loop forever.
  2. super does not change this. The parent's code runs on the same object. Any call inside it without super is a normal dynamic call and starts from the object's real class again.

Trace with a call stack

class P { def tag: String = "p"; def show: String = "[" + tag + "]" }
class Q extends P { override def tag = "q"; override def show = super.show + "Q" }
class R extends Q { override def tag = "r"; override def show = "R" + super.show }
println(new P().show + " " + new Q().show + " " + new R().show)
[p] [q]Q R[r]Q
steprunning bodythiswhat it does
1R.showthe R"R" + super.show: super of R is Q, so call Q.show
2Q.showthe Rsuper.show + "Q": super of Q is P, so call P.show
3P.showthe R"[" + tag + "]": tag is dynamic, this is an R, so R.tag = "r". Returns "[r]"
4back in Q.showthe R"[r]" + "Q" = "[r]Q"
5back in R.showthe R"R" + "[r]Q" = "R[r]Q"

Write the pieces in the position the expression puts them: "R" + super.show puts R in front, super.show + "Q" puts Q at the end.

Where super is written decides which parent

class A0 { def f: String = "A" }
class B0 extends A0 { override def f = "B"; def g: String = super.f + f }
class C0 extends B0 { override def f = "C" }
println(new C0().g + " " + new B0().g)
AC AB

For the C0 object, g is B0's code, and super in B0 means A0, so super.f is "A", not "B" (the parent of the object's class). The plain f is dynamic: "C".

The rule super.m = "run m from the parent of the class this line is written in, on the same object". Everything else inside that body still dispatches on the real object.
Trap Thinking that going through super makes the inner calls static too. In class C extends B { override def x = "X"; override def y = x + super.y }, B's y and A's y still call C's x every time, because this is the C. The other trap is the opposite: treating super.y as dynamic, which would call C's own y again forever.
On the exam super chains with three classes: draw the call stack as in the table above, one row per body entered, with the column this always holding the original object. Fill in dynamic calls from the bottom class's method table; fill in super calls from the parent of the class whose body you are in.
Try it: super chain over three levels
class A { def a: String = "1"; def b: String = a + "<" }
class B extends A { override def a = "2"; override def b = super.b + "B" }
class C extends B { override def a = "3"; override def b = "C" + super.b }
val all: List[A] = List(new A, new B, new C)
println(all.map(_.b))
List(1<, 2<B, C3<B)

Answer. A: A.b = a + "<" with A.a: "1<". B: B.b = A.b + "B"; inside A.b, a is B's: "2<" + "B". C: C.b = "C" + B.b; B.b = A.b + "B"; A.b uses C's a = "3": "C" + "3<" + "B".

Try it: final method plus super
abstract class Greeter { final def greet: String = hello + ", " + hello; def hello: String = "hi" }
class Loud extends Greeter { override def hello = "HI" }
class Louder extends Loud { override def hello = super.hello + "!" }
val gs: List[Greeter] = List(new Loud, new Louder)
println(gs.map(_.greet))
List(HI, HI, HI!, HI!)

Answer. greet is final (nobody may override it), but its calls to hello are normal dynamic calls. For the Louder, hello is Louder's: Loud's "HI" plus "!".

Try it: the parent of the writer, not of the object

With A0, B0, C0 from above, add class D0 extends C0 { override def f = "D" }. What is new D0().g?

AD

Answer. g is still B0's code, so super.f is still A0's "A"; plain f is now D0's "D".

Abstract classes and abstract methods

An abstract method is a method declared without a body: def speak: String. It is a promise: "every concrete animal will have this". An abstract class is a class marked abstract; it may contain abstract methods and cannot be instantiated with new, because some of its methods might have no code.

abstract class Animal {
  def speak: String                           // abstract
  def twice: String = speak + speak           // concrete, calls the abstract one
}

A concrete method calling an abstract one is completely normal (it is the "template" pattern from the slides). By the time any object exists, it is of some concrete subclass that has implemented speak, and dynamic dispatch finds that implementation.

The rules, each with the real compiler message

CodeProblemScala 2.13 says
new Animal (Animal abstract)instantiating an abstract classclass Animal is abstract; cannot be instantiated
class Fish extends Animal without speakconcrete subclass misses an implementationclass Fish needs to be abstract. Missing implementation for member of class Animal: def speak: String = ???
class Plain { def speak: String }abstract member in a class not marked abstractclass Plain needs to be abstract. Missing implementation: def speak: String = ???
final def name: String (no body)abstract and final at onceabstract member may not have final modifier

Things that are allowed (the exam offers them as fake errors):

  • An abstract class with only concrete methods, or with no abstract methods at all (it still cannot be instantiated).
  • An abstract class with constructor parameters: abstract class A(var x: Int); subclasses pass them: class B(xi: Int) extends A(xi).
  • An abstract subclass that does not implement the parent's abstract methods yet (abstract class Pouched extends Mammal).
  • A trait with abstract methods. Traits never need the word abstract, may extend other traits, and an abstract class may extend a trait without implementing its methods.
  • Implementing an abstract method with or without override.
  • An abstract method with any return type (it does not have to "return an abstract type").
  • A variable whose static type is abstract: val a: Animal = new Dog.
The rule A class with an abstract member must itself be declared abstract (Scala does not do it for you). A class that is not abstract must, after inheritance, have a body for every method. Only concrete classes can be created with new.
Trap "Class A must be declared abstract" is only an error when A is not marked abstract. If the code says abstract class A { def b(): Unit }, A is fine; look instead at the first concrete subclass that fails to implement b. And a missing implementation is reported on the class, not on the line where new is called.
Trap (how the real compiler reports) scalac works in rounds. Errors like "cannot be instantiated", "illegal inheritance from final class", "type mismatch" and "reassignment to val" come first; "needs to be abstract" and "cannot override final member" are found in a later round that only runs when the first round was clean. I checked this: a file with all six problems printed only the first two. The exam question "which errors will be reported" means "which of these problems are in the code"; tick all of them.
On the exam: error-hunting checklist Go class by class and ask, in this order:
  1. Does it extend a final class? (illegal inheritance)
  2. Does it override (with or without the keyword, or with a val) a final member? (cannot override final member)
  3. Does it replace a concrete method without override? (override modifier required)
  4. If it is not abstract: after inheritance, does every method have a body? (needs to be abstract)
  5. Is any final member without a body? (abstract member may not have final modifier)
  6. Is a val or a plain constructor parameter assigned? (reassignment to val)
Then look at every new (is the class abstract?) and every typed val/var (is the right side a subtype of the declared type?).
Try it: find all the errors
abstract class Shape(var size: Int) {
  def area(): Int
  final def grow(): Unit = size += 1
  def show(): String = "shape " + area()
}
final class Sq(s: Int) extends Shape(s) {
  override def show(): String = "square"
  override def grow(): Unit = size += 2
}
class BigSq extends Sq(10) {
  def area(): Int = size * size
}
object Use {
  val a: Shape = new Sq(3)
  val b: Shape = new Shape(1)
}

Answer. Four problems:

  • class BigSq extends Sq(10): illegal inheritance from final class Sq.
  • new Shape(1): class Shape is abstract; cannot be instantiated.
  • Sq overrides final grow: cannot override final member: final def grow(): Unit (defined in class Shape).
  • Sq is concrete but has no area() (BigSq's area does not help Sq): class Sq needs to be abstract. Missing implementation for member of class Shape: def area(): Int = ???.

Not errors: new Sq(3) (a final class can be instantiated), Sq overriding the non-final show, the concrete show calling abstract area(), and size += 1 (size is a var). When I ran it, the compiler printed the first two; after removing them it printed the other two.

Try it: true or false?
  1. An abstract class cannot have concrete methods.
  2. A class with one abstract method must be declared abstract.
  3. An abstract class cannot be extended.
  4. abstract class Demo { final def bla(): Int = 3 }: you cannot create an instance of Demo, and bla cannot be overridden.

Answer. 1 false (abstract classes may have concrete methods, even only concrete ones). 2 true. 3 false (being extended is the whole point; you are thinking of final). 4 true, both parts: abstract means no new, final means no override. Subclasses of Demo can still exist.

Final methods and final classes

final is about extension, not about values (for values you have val).

  • A final method cannot be overridden in any subclass.
  • A final class cannot have subclasses.
abstract class Mammal { final def hasNipples = true; def speak: String }
abstract class CatLike extends Mammal { def canPur = true; final def canRoar = !canPur }
class HouseCat extends CatLike { def speak = "Miauw" }
class Lion extends CatLike { override def canPur = false; def speak = "Wraaagh" }
println(new HouseCat().canRoar + " " + new Lion().canRoar + " " + new Lion().hasNipples)
false true true

From the slides: canRoar is final, so no cat can lie about roaring, but the canPur it calls is not final and is dispatched dynamically. The Lion overrides canPur, so the inherited final canRoar gives true.

What final does not mean

StatementTrue?Why
A final class can be instantiatedyesnew Sq(3) is fine; final only stops extends
A final class may override its parent's (non-final) methodsyesfinal limits classes below it, not what it does itself
A final method must be in a final classnothe two are independent
A final class must be abstractnoit is usually concrete (an abstract final class would be useless)
A final method can be called by subclassesyesonly overriding is forbidden
Calls inside a final method are staticnothey dispatch dynamically as always (Greeter and Lion examples)
A val with the same name can replace a final defnoval v = ... overriding final def v gives "cannot override final member"
A trait can have final methodsyesa class mixing it in cannot override them

Real messages: extending a final class gives illegal inheritance from final class Leaf; overriding a final method gives cannot override final member: final def twice: String (defined in class Animal). Overriding a final method without writing override still gives the final error.

Sealed is not final

sealed (week 5) means: direct subclasses may only be defined in the same file. A sealed class normally has subclasses (case classes), can be abstract, and its methods can be overridden. The benefit is that the compiler knows all cases, so a pattern match over them can be checked for exhaustiveness without a default case.

The rule final method: no override below. final class: no subclass. Nothing else changes: you can still call, instantiate, and dynamically dispatch the calls inside.
Trap A method in a final class is not "final" for the purpose of the error message. In final class HedgeHog extends Mammal { override def speakTwice = ... } where Mammal.speakTwice is an ordinary method, overriding it is fine. The only error from a subclass class Sonic extends HedgeHog is "illegal inheritance from final class" (plus whatever it forgot to implement). Also: var x: Int as a constructor parameter is assignable; only val (or a plain parameter) gives "reassignment to val".
On the exam Statement questions ("which are true") and error-hunting questions. For error hunting, mark every final in the code first, then check every class below it for extends of a final class and for any member with the same name as a final member.
Try it: which errors?
trait Winged { def flap(): Unit = println("flap") }
abstract class Bird { final def sing: String = tweet + tweet; def tweet: String }
final class Robin(val n: Int) extends Bird with Winged {
  def tweet = "tjilp"
  def louder(): Unit = n += 1
}
class Baby extends Robin(1)

Answer. Two: n += 1 on a val (the real 2.13 message is value += is not a member of Int ... receiver is not assignable; with n = n + 1 it is literally reassignment to val), and Baby extends final Robin. Not errors: extending a class and mixing in a trait, the trait's concrete flap (nothing to implement), the final sing calling abstract tweet.

Try it: final in a trait
trait D { def h(): String; final def p(): String = "Hello" }
class C extends D { def h() = "h"; def p(): String = "Yo" }

Answer. One error, on p in C: cannot override final member: final def p(): String (defined in trait D). h is implemented, so C does not need to be abstract.

Any: the root of all types

Every type is a subtype of Any. If you write a class without extends, it still extends Any (through AnyRef). Under Any there are two branches: value types (AnyVal: Int, Double, Boolean, Char, ...) which cannot be null, and reference types (AnyRef: String, List, every class you define) which can.

Because everything is an Any, these methods exist on every value:

MethodOverridable?Meaning
equals(that: Any): Booleanyesare these equal? default for classes: same object
hashCode: Intyesmust agree with equals (used by Set/Map)
toString: Stringyestext form, used by println and + with strings
==, !=no, finala == b calls a.equals(b) (null-safe)
isInstanceOf[T], asInstanceOf[T]no, finalrun-time type test, and cast
eq (on AnyRef)no, finalsame object? (reference equality)

toString is overridden like any other method

class Rational(val n: Int, val d: Int) {
  override def toString: String = if (d == 1) n.toString else n + "/" + d
}
val r: Any = new Rational(3, 4)
println(r)
println(new Rational(6, 1).toString + " and " + r.toString)
3/4
6 and 3/4

The static type of r is Any, but toString is dispatched dynamically, so Rational's version runs. The override is required because Any already has a toString with a body.

Type tests and casts

def describe(x: Any): String =
  if (x.isInstanceOf[String]) "string of length " + x.asInstanceOf[String].length
  else "not a string: " + x.toString
println(describe("hallo") + "; " + describe(9))
string of length 5; not a string: 9

isInstanceOf asks about the dynamic class; asInstanceOf tells the compiler "trust me, it is a String" and throws an exception at run time if it is not. The professor calls this the back door of the type system and says to avoid it: if you know the type and the compiler does not, redesign (use a common supertype with a method, or a match). Number conversions like toDouble are fine.

The bottom types (week 6 in detail)

Null is a subtype of every reference type (its one value is null). Nothing is a subtype of every type and has no values; an expression like throw new Error("x") has type Nothing, which is why it fits anywhere.

The rule Any defines equals, hashCode and toString (override them with override) and the final ==, which simply calls equals(that: Any).
Trap You cannot override ==: override def ==(other: Any) gives cannot override final member: final def ==(x$1: Any): Boolean. To change what == does, override equals(other: Any). Anything else you write named == or equals with a different parameter type is an overload, a separate method (next two topics).
On the exam "List(false, "hallo", 3): what is l(0)?" questions (static type Any, so it cannot be used as a Boolean), Rational/toString questions, and the equals/== trap below.
Try it: what does the compiler think?
val l = List(false, "hallo", 3)
println(l)
val x: Boolean = l(0)

Answer. It does not compile, so nothing prints: type mismatch; found: Any, required: Boolean on the last line. Remove that line and it prints List(false, hallo, 3) (via each element's toString).

Try it: toString through a static type Any

With Rational as above: val xs: List[Any] = List(new Rational(1, 2), 5, "five"); println(xs)?

List(1/2, 5, five)

Answer. Printing a list calls toString on each element, dynamically, so the Rational shows as 1/2 even though the element type is Any.

Iterators

An iterator is an object that hands out the elements of something one at a time. The interface has two methods:

abstract class Iterator[A] {
  def hasNext: Boolean      // is there another element?
  def next(): A             // give it, and move forward
}

It is a perfect example of subtype polymorphism: code written against Iterator[Int] works for any iterator, whether it comes from a List, a Set, a Range or your own class.

def addAll(it: Iterator[Int]): Int = {
  var res = 0
  while (it.hasNext) res += it.next()
  res
}
println(addAll(List(1, 2, 3).iterator) + " " + addAll(Set(1, 2, 3).iterator) + " " + addAll((0 until 10 by 2).iterator))
6 6 20

An iterator is a cursor with state

val it = List(10, 20, 30).iterator
it.hasNext   // true
it.next()    // 10
it.next()    // 20
it.hasNext   // true
it.next()    // 30
it.hasNext   // false
it.next()    // throws java.util.NoSuchElementException

(All verified.) hasNext only looks; next() returns the current element and moves the cursor. Once used up, an iterator stays used up.

Writing your own

class Countdown(var cur: Int) extends Iterator[Int] {
  def hasNext: Boolean = cur > 0
  def next(): Int = { val res = cur; cur -= 1; res }   // remember, move, return the remembered one
}
val cd = new Countdown(3)
for (i <- cd) print(i)
println()
for (i <- cd) print(i)     // prints nothing
321

This is the slides' "something funky": a for loop over an iterator consumes it, and the second loop finds cur = 0.

Iterable: something that can give a fresh iterator

class CountdownFrom(n: Int) extends Iterable[Int] {
  def iterator: Iterator[Int] = new Countdown(n)    // a NEW cursor every call
}
val cf = new CountdownFrom(3)
for (i <- cf) print(i)
for (i <- cf) print(i)
321321

Each loop over an Iterable calls iterator and gets its own cursor starting from the beginning. (The Iterable also inherits dozens of methods like map, sum, toList for free.)

Two iterators versus two names for one iterator

Draw boxes and arrows. cf.iterator called twice creates two separate cursor objects. val i3 = i1 creates no object; it is a second arrow to the same cursor (aliasing).

val i1 = cf.iterator
val i2 = cf.iterator
val i3 = i1
print(i1.next()); print(i2.next()); print(i3.next()); print(i2.next()); println(i1.next())
33221
callcursor X (i1 and i3)cursor Y (i2)prints
startcur = 3cur = 3
i1.next()3 → 233
i2.next()23 → 23
i3.next()2 → 122
i2.next()12 → 12
i1.next()1 → 011

Note that calling next() directly does not check hasNext: a RangeIterator with cur < end would happily keep returning values past end if you call next() by hand. Only loops (and library methods) check hasNext.

Anything that walks the iterator consumes it

val big = new Countdown(4)
println(big.size)       // walks to the end to count
println(big.toList)     // nothing left
val big2 = new Countdown(4)
println(big2.toList + " " + big2.toList)
4
List()
List(4, 3, 2, 1) List()

Generated sequences

An iterator does not need a collection behind it. It can compute each element from a state, even forever:

class Squares extends Iterator[Int] {
  var n = 1
  def hasNext = true                        // never ends
  def next(): Int = { val r = n * n; n += 1; r }
}
val sq = new Squares
println(sq.take(4).toList)
println(sq.next())
List(1, 4, 9, 16)
25

A general "state machine" iterator: keep a current state, return it, replace it by f(state), count down.

class StepIter[A](var cur: A, val f: A => A, var count: Int) extends Iterator[A] {
  def hasNext: Boolean = count > 0
  def next(): A = { val res = cur; cur = f(cur); count -= 1; res }
}
val pairs = new StepIter[(Int, Int)]((1, 0), p => (p._1 + 1, p._2 + p._1), 5)
for (e <- pairs.map(_._2)) print(e + " ")
0 1 3 6 10 

Trace with a state table. The returned value is the state before f is applied, so the very first element is the initial state.

callres (returned)new cur = f(cur)count afterprinted _._2
1(1,0)(2,1)40
2(2,1)(3,3)31
3(3,3)(4,6)23
4(4,6)(5,10)16
5(5,10)(6,15)010

With count = 5 you get exactly 5 values. The same idea as a recursive function on lists is unfold: the function returns Some((nextState, output)) to continue or None to stop.

def unfold[S, B](s: S)(f: S => Option[(S, B)]): List[B] =
  f(s) match {
    case Some((next, out)) => out :: unfold(next)(f)
    case None => Nil
  }
println(unfold(1)(x => if (x > 20) None else Some((x * 3, x))))
println(unfold(1)(x => if (x > 20) None else Some((x * 3, x * 3))))
List(1, 3, 9)
List(3, 9, 27)

One small change, different answer: emitting the old state x gives 1, 3, 9 (27 > 20 stops it); emitting the new state x * 3 gives 3, 9, 27. Always check which component of the tuple is the output and which is the next state; different exams put them in different orders.

The rule An iterator is a mutable cursor: next() returns the current element and advances. Every traversal (for loop, size, toList, sum) uses it up. iterator on an Iterable gives a fresh cursor each call; copying a variable gives the same cursor.
Trap (1) Different variable name does not mean different iterator: val it2 = it1 shares the cursor. (2) Two calls to r.iterator are independent, even on the same range, so each starts at start. (3) In a "remember, step, return" next(), the first value is the initial state, not f(initial). (4) A count of 7 gives 7 values, not 8.
On the exam Draw one box per iterator object with its fields (cur, count), and arrows from every variable. For each next() call, update the box the arrow points to and write the printed value. For generated sequences, make the state table above: returned value, new state, counter, printed part.
Try it: a doubling iterator
val doubling = new StepIter[Int](1, x => x * 2, 5)
for (e <- doubling) print(e + " ")
1 2 4 8 16 

Answer. The first value is the initial state 1; each next doubles it; count 5 gives five values.

Try it: toList on a generated iterator

What is new StepIter[(Int, Int)]((1, 0), p => (p._1 + 1, p._2 + p._1), 5).toList?

List((1,0), (2,1), (3,3), (4,6), (5,10))

Answer. Exactly the "res" column of the state table. The second components are the triangular numbers 0, 1, 3, 6, 10.

Try it: unfold with a pair state
unfold((5, 0))(p => if (p._1 == 0) None else Some(((p._1 - 1, p._2 + p._1), p._2 + p._1)))
List(5, 9, 12, 14, 15)

Answer. State (counter, sum). (5,0): new sum 5, emit 5, next (4,5). (4,5): emit 9, next (3,9). (3,9): 12. (2,12): 14. (1,14): 15, next (0,15). (0,15): None, stop. The emitted value is the new sum.

Try it: a running fold (scan)
def scan[A, B](acc: B)(f: (B, A) => B)(l: List[A]): List[B] = l match {
  case h :: t => val nxt = f(acc, h); nxt :: scan(nxt)(f)(t)
  case Nil => Nil
}
scan[Int, Int](10)(_ - _)(List(1, 2, 3))
List(9, 7, 4)

Answer. 10 - 1 = 9, 9 - 2 = 7, 7 - 3 = 4. The start value 10 is not in the output (the library's scanLeft does include it: List(10, 9, 7, 4)). The order is acc - element, as in f(acc, h).

Overloading (ad hoc polymorphism)

Overloading is defining several methods with the same name but different parameter types (or numbers of parameters). They are completely separate methods that happen to share a name.

object Overloading {
  def max(l: List[Int]): Int = l.max
  def max(a: Int, b: Int): Int = if (a > b) a else b
  def max(a: Any): Int = throw new Error("No max for any")
}

The key fact: which overload is called is decided by the compiler, from the static types of the arguments. The compiler collects all overloads that are applicable (each argument's static type is a subtype of the parameter type), and picks the most specific one (the one whose parameter types are subtypes of the others'). The run-time class of the argument plays no role at all.

class Vehicle { def sound: String = "..." }
class Car extends Vehicle { override def sound = "vroom" }
class Tesla extends Car { override def sound = "hum" }
def kind(v: Any): String = "thing"
def kind(v: Vehicle): String = "vehicle(" + v.sound + ")"
def kind(v: Car): String = "car(" + v.sound + ")"

val t1: Tesla = new Tesla
val t2: Car = new Tesla
val t3: Vehicle = new Tesla
val t4: Any = new Tesla
println(kind(t1) + " " + kind(t2) + " " + kind(t3) + " " + kind(t4))
car(hum) car(hum) vehicle(hum) thing

Same object every time, four different static types:

callstatic type of argumentapplicable overloadsmost specificdynamic class (for v.sound)result
kind(t1)TeslaAny, Vehicle, CarCarTeslacar(hum)
kind(t2)CarAny, Vehicle, CarCarTeslacar(hum)
kind(t3)VehicleAny, VehicleVehicleTeslavehicle(hum)
kind(t4)AnyAnyAny(not used)thing

There is no kind(v: Tesla), so even t1 gets the Car overload: the most specific available one. Notice the two moments: the label ("car" or "vehicle") is chosen at compile time from the static type, and the sound ("hum") at run time from the object.

Reassigning a var does not re-choose

var v: Vehicle = new Car
println(kind(v))
v = new Tesla
println(kind(v))
val list: List[Vehicle] = List(new Car, new Tesla)
for (e <- list) print(kind(e) + " ")
vehicle(vroom)
vehicle(hum)
vehicle(vroom) vehicle(hum) 

The static type of v and of the loop variable e is Vehicle, so it is the Vehicle overload every time.

If you want a run-time choice, use a match

for (e <- list) e match {
  case c: Car => print("match car " + c.sound + " ")
  case _ => print("other ")
}
match car vroom match car hum 

A type pattern tests the dynamic class, unlike overloading.

this has a static type too

object Who { def who(x: Any) = "Any"; def who(x: Base) = "Base"; def who(x: Sub) = "Sub" }
class Base { def ask: String = Who.who(this) }
class Sub extends Base
println(new Sub().ask + " " + Who.who(new Sub))
Base Sub

Inside class Base, the compiler types this as Base, so the Base overload is fixed when Base is compiled, even when the object is a Sub.

Several parameters, and passing an overloaded function

sealed abstract class Coin
case class Head() extends Coin
case class Tail() extends Coin
def pick(a: Coin, b: Coin): Coin = a
def pick(a: Head, b: Coin): Coin = b
def pick(a: Tail, b: Tail): Coin = Head()

println(pick(Tail(), Tail()) + " " + pick(Head(), Tail()))
val h: Coin = Head()
println(pick(h, Tail()))
val coins: List[Coin] = List(Tail(), Tail(), Head())
println(coins.foldLeft(Tail(): Coin)(pick))
Head() Tail()
Head()
Tail()

pick(Tail(), Tail()): static types (Tail, Tail); applicable: (Coin,Coin) and (Tail,Tail); most specific (Tail,Tail): Head(). pick(Head(), Tail()): (Head,Coin) is most specific: returns b. pick(h, Tail()): h is a Coin, only (Coin,Coin) applies: returns a, the Head. In the fold, pick is passed as a function where a (Coin, Coin) => Coin is expected. The compiler chooses once, from that expected type: the (Coin, Coin) version, which always returns its first argument, so the accumulator stays the initial Tail(). It never re-chooses per element.

What does not count as overloading

  • Differing only in the return type: def f(a: Int): Int and def f(a: Int): String give method f is defined twice.
  • No unique most specific overload: with f(a: A, b: Any) and f(a: Any, b: A), the call f(new A, new A) gives ambiguous reference to overloaded definition.
The rule Overloading is resolved at compile time: from the static types of the arguments, take the most specific applicable overload. Reassignments, the object's real class and loops do not change that choice.
Trap The answer that says "the Platypus/Stegosaurus/Capybara overload, because the object was created with new Platypus" is always the wrong one. Look at the declaration: val p: Mammal = new Platypus means the Mammal overload. And val v: Any = ... means the Any overload, whatever is inside.
On the exam For each call: (1) write the static type of each argument (go back to its declaration, not its last assignment); (2) cross out overloads that do not accept that type; (3) take the most specific remaining one; (4) inside that body, any method call on the argument (a.x, x.speak) is dynamic again, so use the object's class. When every overload's body just does x.speak, the choice of overload does not even change the output.
Try it: overloads that all delegate
object Talk {
  def talk(a: Animal): String = "animal:" + a.speak
  def talk(d: Dog): String = "dog:" + d.speak
}
val pets: List[Animal] = List(new Dog, new Cat)
println(pets.map(p => Talk.talk(p)))
val d: Dog = new Dog
println(Talk.talk(d))

(Animal, Dog, Cat with speak "...", "woof", "miauw".)

List(animal:woof, animal:miauw)
dog:woof

Answer. p has static type Animal, so both list elements use the Animal overload; the sound comes from the object. d is declared Dog, so the Dog overload.

Try it: same object, new declaration

With kind from above: what are kind(new Car), kind("hello") and kind(42)?

car(vroom) thing thing

Answer. new Car has static type Car. A String and an Int are neither Vehicle nor Car, so only the Any overload applies.

Try it: the static type is the declaration
var x: Vehicle = new Car
var y: Car = new Car
print(kind(x)); y = new Tesla; print(kind(y))
vehicle(vroom)car(hum)

Answer. x is declared Vehicle: Vehicle overload, Car's sound. y is declared Car: Car overload; after the reassignment the object is a Tesla, so the sound is "hum", but the overload stays Car.

Overloading vs overriding (and the == trap)

The two words sound alike and mean opposite things.

OverridingOverloading
WhatA subclass gives a new body for the same signature (same name, same parameter types)Same name, different parameter types, in the same scope
Chosen whenRun timeCompile time
Chosen byDynamic class of the receiver (object before the dot)Static types of the arguments
Kind of polymorphismSubtype polymorphismAd hoc polymorphism
Keywordoverride (required for concrete members)none

The two-step rule for any call

  1. Compile time: using the static type of the receiver and the static types of the arguments, pick the signature (the overload).
  2. Run time: for that exact signature, start at the receiver's dynamic class and take the deepest body.
class Printer { def p(x: Any): String = "Printer.any"; def p(x: String): String = "Printer.str" }
class Fancy extends Printer { override def p(x: Any): String = "Fancy.any" }
val pr: Printer = new Fancy
val s: Any = "hi"
println(pr.p(s) + " " + pr.p("hi") + " " + new Printer().p(s))
Fancy.any Printer.str Printer.any
callstep 1: static types → signaturestep 2: dynamic class → bodyresult
pr.p(s)s is Any → p(Any)Fancy overrides p(Any)Fancy.any
pr.p("hi")String → p(String)Fancy does not override p(String) → Printer'sPrinter.str
new Printer().p(s)p(Any)PrinterPrinter.any

The value of s is a String at run time, but step 1 already fixed p(Any). The overridden p(Any) in Fancy is a different method from p(String).

The classic trap: equals(other: Point)

class Point(val x: Int, val y: Int) {
  def equals(other: Point): Boolean = x == other.x && y == other.y   // OVERLOAD, not override
}
val p1 = new Point(1, 2)
val p2 = new Point(1, 2)
val p2any: Any = p2
println(p1.equals(p2))            // static type Point: picks equals(Point)
println(p1.equals(p2any))         // static type Any: picks Any's equals(Any)
println(p1 == p2)                 // == is Any's final method, calls equals(Any)
println(List(p1).contains(p2))    // library code calls equals(Any)
println(p1 == p1)                 // same object
true
false
false
false
true

Because the parameter type is Point and not Any, this method does not replace Any.equals(that: Any); it is a new overload next to it. Anything that calls equals with static type Any (the == operator, contains, Sets, Maps) still gets the inherited version, which compares references (same object?). Only a direct p1.equals(p2) with a Point-typed argument reaches your method. If you add override, the compiler catches it: method equals overrides nothing. Note: the super classes of class Point contain the following, non final members named equals: def equals(x$1: Object): Boolean.

The correct version overrides with parameter type Any:

class GoodPoint(val x: Int, val y: Int) {
  override def equals(other: Any): Boolean = other match {
    case p: GoodPoint => x == p.x && y == p.y
    case _ => false
  }
  override def hashCode: Int = (x, y).##
}
val g1 = new GoodPoint(1, 2)
val g2: Any = new GoodPoint(1, 2)
println(g1.equals(g2) + " " + (g1 == g2) + " " + List(g1).contains(g2))
true true true

A case class generates exactly this equals (and hashCode and toString) for you: CPoint(1,2) == CPoint(1,2) is true.

"Overriding" == is really overloading it

The slides ask: was the Rational example overriding ==, which is final on Any? No. A method def ==(rhs: Rational) has a different parameter type, so it is an overload.

case class Rational(n: Int, d: Int) extends Comparable[Rational] {
  def compareTo(rhs: Rational): Int = n * rhs.d - rhs.n * d
  def ==(rhs: Rational): Boolean = compareTo(rhs) == 0       // overload of ==
}
val r1 = Rational(1, 2)
val r2 = Rational(2, 4)
val r2any: Any = r2
println((r1 == r2) + " " + (r1 == r2any) + " " + r1.equals(r2))
true false false

r1 == r2: argument typed Rational, so your overload: 1*4 - 2*2 = 0, true. r1 == r2any: argument typed Any, so Any's final ==, which calls the case-class equals comparing fields (1,2) vs (2,4): false. (Side note: the slide version puts def ==(rhs: A) in the generic class Comparable[A]; Scala 2.13 rejects that with "name clash between defined and inherited member ... have same type after erasure", because a type parameter becomes Object at run time. The non-generic version above compiles.)

Both mechanisms in one call

val t2: Car = new Tesla
kind(t2)                          // "car(hum)"

Overload chosen from the static type (Car), then inside the body v.sound is an overridden method chosen from the object (Tesla). Exam answers that mix these up ("tesla", or "car(vroom)") are the distractors.

The rule Signature first, at compile time, from static types (overloading). Body second, at run time, from the receiver's object (overriding). A method with a different parameter type never overrides; it overloads.
Trap def equals(other: Point) compiles silently and looks right in a direct test, but ==, contains, distinct and Sets ignore it. Spot it by the parameter type: anything other than Any is an overload.
On the exam Questions like "what is (over(a), a.x)" with val a: A = new C: the first component uses the static type (over(a: A)), the second the object (C.x). Write both columns next to each other: static A, dynamic C, and read the left one for overloading, the right one for overriding.
Try it: overloads in a class hierarchy
class Shop { def price(x: Any): String = "S-any"; def price(x: Int): String = "S-int" }
class Outlet extends Shop { override def price(x: Int): String = "O-int" }
val sh: Shop = new Outlet
val five: Any = 5
println(sh.price(5) + " " + sh.price(five) + " " + new Shop().price(5))
O-int S-any S-int

Answer. sh.price(5): static Int → price(Int); Outlet overrides it: O-int. sh.price(five): static Any → price(Any); Outlet does not override that one: S-any. Last: Shop's own price(Int).

Try it: tuple of overload and override
class A { def x: String = "a" }
class B extends A { override def x = "b" }
class C extends B { override def x = "c" }
def over(v: Any) = "any"; def over(v: A) = "A"; def over(v: B) = "B"
val q: B = new C
(over(q), q.x)
(B,c)

Answer. Static type of q is B, so over(v: B); dynamic class is C, so q.x is "c".

Try it: will these be equal?

With the broken Point (equals(other: Point)): what are Set(p1, p2).size and p1 != p2?

2 true

Answer. The set has size 2 and p1 != p2 is true. Both go through Any's equals(Any) (and hashCode), which compare references, and p1 and p2 are two different objects.

Recap: Week 4 polymorphism in one screen

Question you askDecided byWhen
Does x.m(y) compile?static type of x (does it have m?) and of ycompile time
Which overload of m?static types of the arguments, most specific applicablecompile time
Which body of m (overriding)?class of the object x points to, deepest definitionrun time
Calls inside that body (y = this.y)?the same object againrun time
super.m?parent of the class where the line is written; this unchangedcompile time (target), run time (inner calls)
Type of f(e)?f's declared result type, type parameters from e's static typecompile time
Type of List(a, b, c) / an if?lowest common supertype (lub); also every type above itcompile time
  • Trace method: two columns (static | dynamic) per variable; a method table (class by method, inherited cells copied from the parent); a call stack for super chains with this fixed; box-and-arrow per iterator object.
  • abstract: no new; abstract members only in abstract classes or traits; concrete subclasses must implement everything; concrete methods may call abstract ones.
  • final: final method = no override below (also not by a val, also not without the keyword); final class = no subclass. Final classes can be instantiated and can override their parents. Abstract members cannot be final. sealed ≠ final.
  • override: required to replace a concrete method; optional when implementing an abstract one.
  • Construction: parent body runs first; overridden methods already dispatch to the subclass, whose vals are still 0/null.
  • Any: equals, hashCode, toString are overridable; ==, !=, isInstanceOf, asInstanceOf are final. == calls equals(Any). equals(other: Point) is an overload and is ignored by ==.
  • Generics: [A] works for every type; [A <: T] lets you call T's methods and keeps the precise type. A bound is never the result type.
  • Iterators: next() returns then advances; traversals consume; iterable.iterator is fresh each time, val b = a is the same cursor; "remember, step, return" yields the initial state first.
  • Languages: inheritance in C#, Java, C++, Scala (not C); generics in Java, C#, Scala (not C, not Python per the course); dynamic dispatch only in Scala and Java, opt-in (virtual) in C++ and C#.

Week 4: Common higher order functions

This block is about the small set of library functions that replace almost every loop: exists, forall, map, foreach, filter, count, flatMap, flatten and the folds. The exam tests them in three ways: trace what a chain prints, translate a for loop into these calls (or back), and decide which implementations or folds are equivalent. Every one of those questions is solved by a mechanical paper method, and this lesson teaches each method.

What a higher order function is, and how to read one

In Scala a function is a value, just like 3 or "hi". You can store it in a val, pass it as an argument and return it. A higher order function is a function that takes another function as an argument (or returns one). map, filter and the folds are all higher order: you hand them a small function and they apply it to the elements of a collection for you.

You write a small function inline as a lambda: x => x + 1 means "given x, return x + 1". The placeholder form _ + 1 means exactly the same thing. Each _ stands for a new parameter, in order, so _ + _ is a function of two arguments, (a, b) => a + b, not x => x + x.

A function that returns a Boolean, like _ > 10, is called a predicate. Most functions in this block take either a predicate (exists, forall, filter, count, takeWhile) or a transformation (map, flatMap) or a combining operator (the folds).

val twice = (f: Int => Int) => (x: Int) => f(f(x))   // takes a function, returns a function
println(twice(_ + 3)(1) + " " + twice(_ * 2)(5))
val isBig: Int => Boolean = _ > 10
println(List(5, 50).map(isBig) + " " + List(5, 50).filter(isBig))
println(List(1, 2, 3).map(x => x + x))
7 20
List(false, true) List(50)
List(2, 4, 6)

All the functions in this block work on immutable collections: they never change the list you call them on, they build and return a new value. List(1,2,3) is a list. A range like 0 until 9 (0 to 8, end excluded) or 1 to 4 (1 to 4, end included) is a Range; when you map or filter a range you get a Vector back, which is why answers often print as Vector(...).

The rule Read a chain like l.map(f).filter(p).sum from left to right, one stage at a time. Each stage takes the whole result of the previous stage. _ means "the next parameter", so count the underscores to know how many arguments the lambda takes.
Trap List(1, 2).map(_ + _) does not double each element. It is a two argument function, and map wants a one argument function, so it does not compile: error: missing parameter type for expanded function ((x$1, x$2) => x$1.$plus(x$2)). To double, write x => x + x or _ * 2.
On the exam "What is the type of f?" questions give val f = l => l.map(...)... and ask which parameter type makes the body work. (Real Scala would refuse it with error: missing parameter type; the exam means "which type must l have".) Method: look at the deepest use. If you see _.map(_ + 1) inside a map, each element is itself a list, so l: List[List[Int]]. Then walk left to right and write the type after every stage: map(_.sum) gives List[Int], filter keeps the type, sum gives Int, flatMap or flatten removes one list layer, map(x => List(x)) adds one.
Try it: what is the type of g?
val g = (x: List[List[Int]]) => x.map(_.filter(_ > 0)).filter(_.nonEmpty).map(_.max).map(n => List(n, n))

Write the type after each stage: x is List[List[Int]]. map(_.filter(_ > 0)): each inner list stays a list, so still List[List[Int]]. filter(_.nonEmpty): same type. map(_.max): each inner list becomes one Int, so List[Int]. map(n => List(n, n)): each Int becomes a list, so List[List[Int]]. Answer: List[List[Int]] => List[List[Int]]. The trap is to stop after max.

Try it: what does this print?
def applyAll(fs: List[Int => Int], x: Int): List[Int] = fs.map(f => f(x))
println(applyAll(List(_ + 1, _ * 2, x => x * x), 4))
List(5, 8, 16)

The list holds three functions. map calls each one on 4: 4 + 1 = 5, 4 * 2 = 8, 4 * 4 = 16. A list of functions is a normal list; its elements just happen to be function values.

exists and forall

l.exists(p) asks: "is there at least one element for which p is true?" l.forall(p) asks: "is p true for every element?" Both return a single Boolean, not a list.

The empty list is where people go wrong. exists needs a witness, an element that proves the claim; an empty list has none, so List().exists(p) is false. forall is false only if you can find a counterexample, an element that breaks the claim; an empty list has none, so List().forall(p) is true. This is called vacuous truth: "every element of an empty list is even" is true because there is nothing to check.

val nums = List(3, 8, 5, 10)
println(nums.exists(_ > 9) + " " + nums.forall(_ > 2) + " " + nums.forall(_ % 2 == 0))
println(List[Int]().exists(_ > 0) + " " + List[Int]().forall(_ > 0))
println(List(List(1, 3), List()).exists(_.forall(_ % 2 == 0)))
println(List(List(1, 3), List()).forall(_.exists(_ % 2 == 0)))
true true false
false true
true
false

Line 3: the outer exists looks for one inner list where all elements are even. List(1,3) fails, but List() passes vacuously, so the answer is true. Line 4: the outer forall needs every inner list to contain an even number; List() contains nothing, so it fails, and the answer is false.

Another way to see it. exists is a chain of || starting from false, and forall is a chain of && starting from true:

// List(a, b, c).exists(p)  ==  false || p(a) || p(b) || p(c)
// List(a, b, c).forall(p)  ==  true  && p(a) && p(b) && p(c)
def ex[A](p: A => Boolean, l: List[A]): Boolean = l match {
  case Nil    => false
  case h :: t => p(h) || ex(p, t)
}

With zero elements only the start value is left: false for exists, true for forall. If you see recursion with case Nil => true and p(head) && rec(tail), that is forall; with Nil => false and || it is exists.

Short circuit. Both stop as soon as the answer is known: exists at the first true, forall at the first false. You only see this when the predicate has a side effect, such as printing:

def loud(x: Int): Boolean = { print(s"[$x]"); x > 4 }
val r1 = List(1, 7, 2, 9).exists(loud); println(" " + r1)
val r2 = List(1, 7, 2, 9).forall(loud); println(" " + r2)
val r3 = List(1, 7, 2, 9).filter(loud).nonEmpty; println(" " + r3)
[1][7] true
[1] false
[1][7][2][9] true
The rule exists = "some element passes", false on empty. forall = "no element fails", true on empty. They are each other's mirror: l.forall(p) == !l.exists(x => !p(x)).
Trap Three classic slips. (1) Thinking List().forall(p) is false. (2) Writing l.map(p).length > 0 as a replacement for exists: map never removes anything, so its length is just l.length and p is ignored. List(5).map(_ => false).length is 1 while List(5).exists(_ => false) is false. (3) Thinking a for ... yield returns a Boolean: for (x <- l if p(x)) yield true is a list of trues.
On the exam Two patterns. (a) "On which inputs does f return true?" with nested lists: handle the outer function first, then the inner one, and check the empty outer list and an empty inner list on purpose. Inputs of the wrong type (a flat list where a list of lists is needed) are never correct. (b) "Which implementations are equivalent to exists / forall?" Test each candidate on three tiny lists: the empty list, a one element list where p is true, and a one element list where p is false. Correct forms: a loop that returns early, l.filter(p).length > 0 (or nonEmpty), l.map(p).fold(false)(_ || _); for forall: l.filter(p).length == l.length, l.filter(!p(_)).length == 0, l.map(p).fold(true)(_ && _). A fold with the wrong start (fold(false)(_ && _)) is always false.
Try it: four checks on one grid
val grid = List(List(2, 4), List(), List(3, 6))
println(grid.exists(_.forall(_ % 2 == 0)) + " " + grid.forall(_.exists(_ % 2 == 0)) + " " +
        grid.forall(_.forall(_ % 2 == 0)) + " " + grid.exists(_.exists(_ % 2 == 1)))
true false false true

(1) Is there a row that is all even? List(2,4) is, so true. (2) Does every row contain an even number? List() contains nothing, so false. (3) Is every number even? Row 3 has 3, so false. (4) Is there an odd number somewhere? 3, so true.

Try it: map, filter, then exists or forall
println(List(1, 2, 3).filter(_ > 1).map(_ * 2).exists(_ > 5))
println(List(1, 2, 3).map(_ * 2).filter(_ > 1).forall(_ > 3))
true
false

Line 1: filter gives 2, 3, map gives 4, 6, and 6 > 5, so true. Line 2: map gives 2, 4, 6, filter(_ > 1) keeps all three, and 2 is not > 3, so false. Order of stages matters: the filter tests whatever values reach it.

Try it: which prints happen?

With the loud predicate above, what does List(5, 1, 9).forall(loud) print? loud(5) prints [5] and is true, so continue. loud(1) prints [1] and is false, so forall stops. Output: [5][1], result false. 9 is never looked at.

map

l.map(f) applies f to every element and returns a new collection of the results, same length, same order. The slide shows it as a machine that puts a smiley face on every shape: three shapes in, three smiley shapes out. If f: A => B then List[A].map(f): List[B].

println(List("hi", "scala", "fp").map(_.length))
println(List(List(1,2), List(3)).map(_.map(_ * 10)))
println(List(List(1,2), List(3)).map(_.sum))
println((1 to 4).map(_ * 2))
List(2, 5, 2)
List(List(10, 20), List(30))
List(3, 3)
Vector(2, 4, 6, 8)

As a recursive function, map is recognisable by its shape: one output element per input element, built with ::.

def mp[A, B](f: A => B, l: List[A]): List[B] = l match {
  case Nil    => Nil
  case h :: t => f(h) :: mp(f, t)
}

map on other containers. map exists on anything that "holds" values. Option holds zero or one value (Some(x) or None); Either holds a Left or a Right, and in Scala 2.13 map only touches a Right. The rule is always the same: the shape (the constructor) stays, only the value inside changes.

println(Some(2).map(_ * 4) + " " + (None: Option[Int]).map(_ * 4))
val e1: Either[String, Int] = Right(2)
val e2: Either[String, Int] = Left("oops")
println(e1.map(_ * 4) + " " + e2.map(_ * 4))
Some(8) None
Right(8) Left(oops)

Map fusion (slide): for pure f and g, l.map(f).map(g) == l.map(f andThen g). Two passes give the same list as one pass that does f then g. This is an example of equational reasoning: replacing code by code that is equal. The professor adds a condition: only if f and g are pure. A pure function has no side effects (no printing, no changing variables). With side effects the results are equal but the order of the effects differs:

def f(x: Int) = { print(s"f$x "); x + 1 }
def g(x: Int) = { print(s"g$x "); x * 10 }
val a = List(1, 2, 3).map(f).map(g); println("=> " + a)
val b = List(1, 2, 3).map(x => g(f(x))); println("=> " + b)
f1 f2 f3 g2 g3 g4 => List(20, 30, 40)
f1 g2 f2 g3 f3 g4 => List(20, 30, 40)

Paper method for chains: the pipeline table. Write one row per input element and one column per stage. A filter crosses out a row; a map rewrites the value. Never filter on the original values when the filter comes after a map.

println((0 until 6).map(_ * 3).map(n => if (n % 2 == 0) n / 2 else n).filter(_ > 3).map(_ - 1))
inputmap(_ * 3)map(even halves)filter(_ > 3)map(_ - 1)
000out
133out
263out
39998
412665
515151514
Vector(8, 5, 14)
The rule map never changes the number of elements or their order, and never removes a layer of nesting. f: A => B turns List[A] into List[B]. If f returns a list, you get a list of lists.
Trap (1) Expecting map to flatten: List(1,3).map(x => List(x, x+1)) is List(List(1, 2), List(3, 4)), not List(1,2,3,4). (2) In a chain, applying a later filter to the original numbers instead of the transformed ones. (3) On an Either, applying the left function to a Right value or switching the constructor; a hand written bimap(e, f, g) applies f to a Left and g to a Right and keeps the side.
On the exam "What does this print" with r.map(...).map(...).filter(...).map(...) on a range: draw the pipeline table, and check the range bounds first (until excludes the end, to includes it). Also: "which function is this?" for a recursive definition (shape f(h) :: rec(t) is map), and "which expressions are equivalent" using map fusion: l.map(f).filter(g).exists(h) equals l.exists(x => g(f(x)) && h(f(x))) for pure functions.
Try it: map fusion with a log
var log = List[String]()
def f(x: Int) = { log = log :+ s"f$x"; x * 2 }
def g(x: Int) = { log = log :+ s"g$x"; x + 1 }
val r1 = List(1, 2).map(f).map(g); println(r1 + " " + log)
log = Nil
val r2 = List(1, 2).map(x => g(f(x))); println(r2 + " " + log)
List(3, 5) List(f1, f2, g2, g4)
List(3, 5) List(f1, g2, f2, g4)

Same lists (1*2+1 = 3, 2*2+1 = 5). The first version finishes the whole first map before the second starts, so both f calls come first. The fused version does f then g per element. g receives the output of f (2 and 4), not the original element.

Try it: a pipeline on a range

What is (1 to 5).map(_ + 1).filter(_ % 2 == 0).map(_ * 10)? Range 1..5 (inclusive). +1: 2,3,4,5,6. Keep even: 2,4,6. Times 10: Vector(20, 40, 60).

foreach

l.foreach(f) calls f on every element, in order, only for its side effect, and returns Unit. Unit is Scala's "no useful value" type; it has one value, written (). So whatever f returns is thrown away. foreach is what you use to print or to update a var.

val u = List(1, 2, 3).foreach(x => x * 100)
println(u)
var total = 0
List(4, 5, 6).foreach(x => total += x)
println(total)
List("a", "b", "c").foreach(print); println()
()
15
abc

Compare with map, which keeps the return values. If you map a printing function you get the prints and a list of units:

val mapped = List(1, 2).map(x => println("side " + x))
println(mapped)
side 1
side 2
List((), ())
The rule foreach = "do this for every element", result (). map = "turn every element into something", result a collection. A for loop without yield is a foreach.
Trap List(1,3).foreach(x => List(x, x + 1)) does not build anything; it returns (). Also note print (no newline) versus println: foreach(print) on 3,4,3,3,3,8 prints 343338 with no separators, which is how some exam answers are written.
On the exam foreach appears as the final printing step of a chain, as a wrong option for "which expression gives this list" (it gives Unit), and inside count implementations that bump a var: { var x = 0; l.filter(f).foreach(_ => x += 1); x } is a correct count.
Try it: summing with foreach
var n = 0
List(3, 4, 5).foreach(x => if (x % 2 == 1) n += x)
println(n)
8

Odd elements are 3 and 5, so n goes 0, 3, (4 skipped), 8. The foreach itself returned (), but the var outside it changed.

Try it: a for loop that prepends
var acc = ""
for (w <- List("a", "bb", "ccc"); if w.length != 2) acc = w + acc
println(acc)
ccca

The guard drops "bb". Then acc becomes "a", then "ccc" + "a" = "ccca". Each new word goes in front, so the order is reversed.

filter, takeWhile, dropWhile

All three take a predicate and return a sub-list, keeping the original order. They differ in when they look:

  • filter(p) looks at every element and keeps those where p is true. (filterNot(p) keeps those where it is false.) The slide picture is a coffee filter: everything is poured through, the grounds stay behind.
  • takeWhile(p) walks from the front and keeps elements until the first one where p is false, then stops for good. Later elements are never kept, even if they pass.
  • dropWhile(p) walks from the front and throws away elements until the first one where p is false; that element and everything after it is kept.

So takeWhile(p) ++ dropWhile(p) is always the whole list (that pair is span). On a sorted list takeWhile and filter often agree; on an unsorted list they do not.

val xs = List(1, 2, 5, 1, 7, 2)
println(xs.filter(_ < 3))
println(xs.takeWhile(_ < 3))
println(xs.dropWhile(_ < 3))
println(xs.filterNot(_ < 3))
println(xs.takeWhile(_ > 3) + " " + xs.dropWhile(_ > 3))
println(xs.span(_ < 3))
println(List(1,2,3,4,5).takeWhile(_ < 3) + " " + List(1,2,3,4,5).filter(_ < 3))
List(1, 2, 1, 2)
List(1, 2)
List(5, 1, 7, 2)
List(5, 7)
List() List(1, 2, 5, 1, 7, 2)
(List(1, 2),List(5, 1, 7, 2))
List(1, 2) List(1, 2)

Line 5 is the subtle one: the very first element, 1, fails _ > 3, so takeWhile takes nothing and dropWhile drops nothing.

element125172
_ < 3TTFTFT
filterkeepkeepkeepkeep
takeWhilekeepkeepSTOP
dropWhiledropdropkeepkeepkeepkeep
The rule filter: test every element. takeWhile: the longest prefix that passes. dropWhile: everything from the first failure on. Only filter can keep an element that comes after a failing one.
Trap Treating takeWhile as filter on unsorted input, and forgetting that dropWhile keeps later elements that would pass (List(5, 1, 7, 2) above still contains 1 and 2). Also: in a chain, the filter sees the values produced by the stage before it, not the original values.
On the exam filter appears everywhere in chains and equivalence questions; takeWhile/dropWhile as "what does this print" variants. Method: write the predicate result (T/F) under every element, then apply the table above. Watch strict versus non-strict comparisons: e > 10 drops 10 itself.
Try it: strings
val strs = List("apple", "fig", "kiwi", "banana")
println(strs.takeWhile(_.length > 3) + " " + strs.dropWhile(_.length > 3) + " " + strs.filter(_.length > 3))
List(apple) List(fig, kiwi, banana) List(apple, kiwi, banana)

Lengths 5, 3, 4, 6, so the test gives T, F, T, T. takeWhile stops at "fig". dropWhile keeps from "fig" on. filter skips only "fig".

Try it: combine them
println(List(4, 1, 6, 3, 8).dropWhile(_ % 2 == 0).takeWhile(_ < 5))
println(List(4, 1, 6, 3, 8).filter(_ % 2 == 0).takeWhile(_ < 7))
List(1)
List(4, 6)

Line 1: drop the leading evens (just 4), leaving 1, 6, 3, 8; take while < 5: 1, then 6 stops it. Line 2: keep evens 4, 6, 8; take while < 7: 4, 6, then 8 stops it.

count

l.count(p) returns how many elements satisfy p, as an Int. It is the same as l.filter(p).length, but without building the intermediate list. The exam mostly asks you to recognise correct and incorrect ways to write count, so here are both kinds side by side.

val ys = List(3, 8, 5, 10, 6)
println(ys.count(_ > 5))                                          // library
println(ys.filter(_ > 5).length)                                  // keep the matches, measure
println(ys.map(x => if (x > 5) 1 else 0).sum)                     // 1 per match, add up
println((for (y <- ys if y > 5) yield 1).sum)                     // guard, then 1 per match
println(ys.foldLeft(0)((c, y) => if (y > 5) c + 1 else c))        // accumulator
println(ys.map(_ > 5).length)                                     // WRONG
println((for (y <- ys) yield y > 5).length)                       // WRONG
println(ys.foldLeft(0)((c, y) => if (y > 5) 1 else 0))            // WRONG
println(ys.map(_ > 5))
3
3
3
3
3
5
5
1
List(false, true, false, true, true)

Why are the last three wrong? map(_ > 5) produces one Boolean per element (last line), and length counts the falses too, so you get the length of the list. A for ... yield without a guard is just a map, same problem. The broken fold returns 0 or 1 for the current element and ignores the accumulator c, so the result only describes the last element (6 > 5, so 1).

The rule A correct count either removes the non-matching elements before measuring (filter, a guard), or turns each element into 1 or 0 and adds, or carries an accumulator that it actually uses (c + 1 or c).
Trap "map to Booleans then length" looks like counting but counts everything. A fold step must use the accumulator; (c, e) => if (p(e)) 1 else 0 throws it away. A local var counter inside a function is fine and gives the same result; local mutation does not make a function wrong.
On the exam "Which implement count?" Test each option on a 2 element list where the first matches and the second does not (answer must be 1). map(p).length gives 2, the broken fold gives 0. Then check types: sum on a List[Boolean] does not work, booleans must be turned into numbers first.
Try it: which give 2?
val l = List(7, 2, 9); val p: Int => Boolean = _ > 5
println(l.filter(p).size + " " + l.map(p).length + " " +
        l.foldLeft(0)((c, e) => c + (if (p(e)) 1 else 0)) + " " + l.foldLeft(0)((c, e) => if (p(e)) 1 else c))
2 3 2 1

(a) keeps 7, 9: 2. (b) three Booleans: 3. (c) 0 then 1, 1, 2: 2. (d) uses c only when p fails and resets to 1 when it passes: 1, 1, 1: 1. Correct: (a) and (c).

Try it: count with strings
println(List("apple", "fig", "kiwi", "banana").count(_.contains("a")))
2

apple and banana contain an "a"; fig and kiwi do not.

For loop desugaring: the whole recipe

Syntactic sugar is nicer syntax for something you could already write. A Scala for loop is sugar: the compiler rewrites ("desugars") it into calls to foreach, map, flatMap and withFilter before doing anything else. The slides summarise: for loop = foreach, filter and for yield loop = map, filter, flatten, flatMap.

Vocabulary: in for (x <- l; if x > 1; y <- m) yield x * y, each name <- collection is a generator, each if cond is a guard (the ; before an if may be left out), and yield e says "collect the values of e".

You do not have to trust me on the rewrite: Scala can print what it turns a for into. This is real compiler output (only the noise prefixes $anon. and `package`. are removed):

for (x <- l) println(x)
for (x <- l if x > 1) println(x)
for (x <- l) yield x * 2
for (x <- l if x > 1) yield x * 2
for (x <- l; y <- m) yield x + y
for (x <- l; if x > 1; y <- m) yield x + y
for (x <- l; y <- m; z <- List(x, y)) yield z
for (x <- l; y <- m) println(x + y)
l.foreach(((x) => Predef.println(x)))
l.withFilter(((x) => x.>(1))).foreach(((x) => Predef.println(x)))
l.map(((x) => x.*(2)))
l.withFilter(((x) => x.>(1))).map(((x) => x.*(2)))
l.flatMap(((x) => m.map(((y) => x.+(y)))))
l.withFilter(((x) => x.>(1))).flatMap(((x) => m.map(((y) => x.+(y)))))
l.flatMap(((x) => m.flatMap(((y) => List.apply(x, y).map(((z) => z))))))
l.foreach(((x) => m.foreach(((y) => Predef.println(x.+(y))))))</code>

The recipe, applied from left to right:

  1. No yield: every generator becomes foreach, nested inside the previous one.
  2. With yield: the last generator becomes map; every earlier generator becomes flatMap.
  3. A guard becomes .withFilter(cond) on the generator just before it. For the result, withFilter is the same as filter (it only skips building an in-between list), so exam answers write filter.
  4. The body (after yield, or the loop body) goes inside the innermost lambda.
  5. yield for (...) yield ... is not a second generator: the inner for is the body. The outer generator then becomes map (it is the last generator of the outer loop), and the result is nested.

Why flatMap for the earlier generators? For each x, the rest of the loop produces a whole collection (all the y results for that x). map would give a list of those collections; flatMap glues them into one flat collection, which is what a single for with several generators returns.

The result type follows the first generator. The outermost call is a method of the first collection, so a for over a List gives a List, over a Range gives an IndexedSeq (prints as Vector), over an Option gives an Option.

The rule Last generator: map (or foreach). Earlier generators: flatMap (or foreach). Guard: filter/withFilter on the generator right before it. Nested yield for: outer map, nested result.
Trap "There are two loops, so it must be flatMap." Only two generators in the same for give flatMap. Two fors nested with yield for give map of map, and a nested result.
On the exam Desugaring is asked in both directions. For "which expression equals this for": write it with the recipe yourself first, then compare. Check three things per option: (1) which method is outermost (map vs flatMap), (2) which collection each filter is on (the guard filters the generator right before it), (3) whether the filter comes before the map (it must, because the guard tests the original element). For "which for equals this chain", go backwards: an outer flatMap means a second generator in the same for; an outer map whose body is a collection means yield for.
Try it: desugar by hand

Desugar for (x <- l; y <- m; if x > 1) yield x * y. Generators: x (not last: flatMap), y (last: map). The guard comes after y, so it filters m, even though it only mentions x. The compiler says:

l.flatMap(((x) => m.withFilter(((y) => x.>(1))).map(((y) => x.*(y)))))

With l = List(1,2,3), m = List(10,20) it gives List(20, 40, 30, 60), the same as putting the guard right after x (l.withFilter(x => x > 1).flatMap(...)). Different code, same result: for x = 1 the inner list is emptied, for other x nothing is removed.

Try it: nested yield

Desugar for (x <- l if x > 1) yield for (y <- m if y > x) yield (x, y). The outer for has one generator, so it is a map, with the inner for as its body:

l.withFilter(((x) => x.>(1))).map(((x) => m.withFilter(((y) => y.>(x))).map(((y) => Tuple2.apply(x, y)))))

Exam notation: l.filter(_ > 1).map(x => m.filter(_ > x).map(y => (x, y))), a list of lists.

For loop without yield becomes foreach

A for without yield runs its body for its side effect and returns (). Each generator becomes a foreach, nested in the order written, so the first generator is the outer loop and the last one changes fastest, like the digits of an odometer.

val l = List(1, 2, 3)
val m = List(10, 20)
for (x <- l; y <- m) print(s"($x,$y)"); println()
l.foreach(x => m.foreach(y => print(s"($x,$y)"))); println()
val unitResult = for (x <- l) x * 2
println(unitResult)
(1,10)(1,20)(2,10)(2,20)(3,10)(3,20)
(1,10)(1,20)(2,10)(2,20)(3,10)(3,20)
()

A for works on anything that has these methods, not only lists. An Iterator (a one-pass producer of values, see the iterators lesson) has foreach and map, so for (x <- Iterator(5, 6, 7).map(_ * 2)) print(x) becomes Iterator(5, 6, 7).map(_ * 2).foreach(x => print(x)) and prints 101214 (run).

The same nesting order holds for for ... yield; only the method names change. So the odometer method below also answers "what is the value" questions with several generators.

Paper method: the odometer table. One column per generator, left to right. The rightmost column runs through all its values before the column to its left moves one step. Evaluate each guard as soon as all the names it uses are known; if it is false, skip that row and everything nested under it. Write down the yielded value for each surviving row, top to bottom.

println(for (i <- 1 to 3; j <- 1 to 3; if i <= j) yield (i, j))
println(for (j <- 1 to 3; i <- 1 to 3; if i <= j) yield (i, j))
Vector((1,1), (1,2), (1,3), (2,2), (2,3), (3,3))
Vector((1,1), (1,2), (2,2), (1,3), (2,3), (3,3))

Same pairs, different order: in the second loop j is the outer generator, so the pairs are grouped by j.

The rule for (a <- A; b <- B) body = A.foreach(a => B.foreach(b => body)). First generator outermost, last generator innermost and fastest. A guard is a withFilter (think: filter) on the generator before it.
Trap (1) Swapping the nesting order. (2) Treating to as exclusive: 1 to 4 is 1,2,3,4 and 0 until 3 is 0,1,2. (3) Yielding the wrong variable: read the yield expression last and carefully. (4) A later generator may depend on earlier names (z <- (i - j) to (i - j + 1)), so its range is different for each row.
On the exam "Value of test?" with three generators and a guard: build the odometer table, apply the guard as early as possible (a guard on i + j placed before z removes rows before z is generated, which saves work), then list the yielded values in row order. Also: equivalence questions where a for loop with return inside implements exists or forall.
Try it: dependent generator
println(for (i <- 1 to 3; j <- i to 3) yield i * j)
Vector(1, 2, 3, 4, 6, 9)
ij runs overi * j
11, 2, 31, 2, 3
22, 34, 6
339
Try it: guard on the outer generator
println(for (a <- List(3, 6, 9); if a % 2 == 1; b <- 1 to 2) yield a + b)
List(4, 5, 10, 11)

The guard drops a = 6 before b is generated. a = 3: 4, 5. a = 9: 10, 11. Result type is List because the first generator is a List.

for ... yield with one generator: map and filter (withFilter)

With one generator, for (x <- l) yield e is l.map(x => e). Add a guard and it becomes l.withFilter(x => cond).map(x => e), which gives the same result as l.filter(x => cond).map(x => e). The filter always comes before the map, because the guard tests the original element x, not the yielded value.

val l = List(1, 2, 3)
println(for (x <- l if x != 2) yield x * 10)
println(l.withFilter(_ != 2).map(_ * 10) + " " + l.filter(_ != 2).map(_ * 10))
List(10, 30)
List(10, 30) List(10, 30)

What is withFilter? filter builds a new, shorter list right away. withFilter does not build anything: it remembers the condition and applies it during the following map/flatMap/foreach. That saves one intermediate list. The values that come out are the same, so for "which expression is equivalent" questions filter and withFilter are interchangeable.

Nested yield for. If the yielded expression is itself a for ... yield, the outer loop maps each element to a whole inner collection. Result: a collection of collections.

println(for (i <- 1 to 3) yield for (j <- 1 to 3 if i < j) yield (i, j))
Vector(Vector((1,2), (1,3)), Vector((2,3)), Vector())

Note the empty Vector() for i = 3: the outer loop has no guard, so every i produces one inner vector, even an empty one. Desugared: (1 to 3).map(i => (1 to 3).filter(j => i < j).map(j => (i, j))).

The rule for (x <- l if p(x)) yield f(x) = l.filter(p).map(f). Never l.map(f).filter(p): that tests p on the transformed values. yield collects, it never adds up.
Trap (1) Applying the guard to the yielded value (s + e) instead of to e. (2) Expecting a number from yield (a sum); it is always a collection. (3) In nested loops, moving the outer guard after the outer map: then it would test inner results, not the original rows. (4) Writing if outside the parentheses, like for (e <- a; v <- b) if (e + v > 7) yield e * v: that is not a guard, and it does not compile (error: illegal start of simple expression).
On the exam Most desugaring questions are this pattern twice: outer filter(...).map(row => inner) where inner is row.filter(...).map(...). For each option check: is the outer method map (nested result, matches a declared type like List[List[Int]] or Seq[Seq[...]]) or flatMap (flat result)? Is each filter before its map? Is each filter on the right level (rows vs elements)?
Try it: keep the rows
println(for (a <- List(List(1, 2), List(5), List(7, 8, 9)) if a.length > 1) yield a.map(_ * 2))
List(List(2, 4), List(14, 16, 18))

One generator, so filter(_.length > 1).map(a => a.map(_ * 2)). List(5) is dropped by the guard. The yielded value is a list, so the result is a list of lists.

Try it: guard on an Option
val opt: Option[Int] = Some(5)
println(for (x <- opt if x > 9) yield x)
None

The first generator is an Option, so the result is an Option. The guard fails, so the value is removed and Some(5) becomes None (an empty Option).

flatten (and flatMap = map then flatten)

flatten removes exactly one layer of nesting: it takes a list of lists and glues the inner lists together, in order. Its signature is def flatten[A](l: List[List[A]]): List[A]. Empty inner lists simply disappear, and a list of Options keeps only the Some values.

println(List(List(1, 2), List(), List(3)).flatten)
println(List(Some(1), None, Some(3)).flatten)
println(List(List(List(1), List(2)), List(List(3))).flatten)
List(1, 2, 3)
List(1, 3)
List(List(1), List(2), List(3))

flatMap(f) is map(f) followed by flatten. So f must return a collection (f: A => List[B]), and the result is List[B]. Compare the two signatures the exam uses:

def map[A, B]    (l: List[A], f: A => B)       : List[B]
def flatMap[A, B](l: List[A], f: A => List[B]) : List[B]
def flatten[A]   (l: List[List[A]])            : List[A]
println(List(1, 3).flatMap(x => List(x, x + 1)) + " " + List(1, 3).map(x => List(x, x + 1)))
println(List(1, 3).map(x => List(x, x + 1)).flatten)
println(List(1, 2, 3).flatMap(x => if (x > 1) Some(x) else None))
List(1, 2, 3, 4) List(List(1, 2), List(3, 4))
List(1, 2, 3, 4)
List(2, 3)

If f does not return a collection, flatMap does not compile: List(1, 2).flatMap(x => x * 2) gives error: type mismatch; found: Int, required: scala.collection.IterableOnce[?].

Flatten with folds. Gluing lists is ++, and the empty list is the neutral start, so all three folds flatten correctly: l.foldLeft(List[A]())(_ ++ _), l.foldRight(List[A]())(_ ++ _) and l.fold(List[A]())(_ ++ _). The fold sections explain why they agree (++ is associative and List() is its identity element). But swapping the operands does change the result, because ++ is not commutative:

println(List(List(1,2), List(3)).foldRight(List[Int]())(_ ++ _) + " " + List(List(1,2), List(3)).foldLeft(List[Int]())((a, x) => x ++ a))
List(1, 2, 3) List(3, 1, 2)

flatMap on your own types. Any type can define flatMap: "replace every element by a structure of the same kind, then merge". On a binary tree, it replaces every leaf by a whole tree:

sealed abstract class Tree[A] {
  def flatMap[B](f: A => Tree[B]): Tree[B] = this match {
    case Node(l, r) => Node(l.flatMap(f), r.flatMap(f))
    case Leaf(x)    => f(x)
  }
  def reduce(f: (A, A) => A): A = this match {
    case Node(l, r) => f(l.reduce(f), r.reduce(f))
    case Leaf(x)    => x
  }
}
case class Node[A](l: Tree[A], r: Tree[A]) extends Tree[A]
case class Leaf[A](x: A) extends Tree[A]
def split(x: Int): Tree[Int] = Node(Leaf(x - 1), Leaf(x * 10))
val t1 = Leaf(3).flatMap(split)
val t2 = t1.flatMap(split)
println(t1 + " " + t1.reduce(_ + _))
println(t2 + " " + t2.reduce(_ + _))
Node(Leaf(2),Leaf(30)) 32
Node(Node(Leaf(1),Leaf(20)),Node(Leaf(29),Leaf(300))) 350

The second flatMap hits both leaves: 2 becomes (1, 20) and 30 becomes (29, 300). reduce then combines the leaves following the tree: (1 + 20) + (29 + 300) = 350.

The rule flatten: List[List[A]] => List[A], one layer, order kept. flatMap(f) = map(f).flatten, and f must return a collection.
Trap (1) Picking flatten(l: List[A]): List[A] as the signature: there is no layer to remove. (2) Confusing flatMap's A => List[B] with map's A => B, or with a fold's (B, A) => B. (3) Thinking flatten removes all nesting at once. (4) On trees: applying the second flatMap only to one leaf.
On the exam Signature questions ("which header fits flatten / flatMap"), "which expressions give List(1,2,3,4)" (flatMap, map + flatten, a foldLeft that appends with ++ in the right order), "which are equivalent to this flatten loop", and tree flatMap + reduce traces: draw the tree after each flatMap, then add up the leaves.
Try it: flatMap vs map
println(List(1,2,3).flatMap(x => List.fill(x)(x)) + " " + List(1,2,3).map(x => List.fill(x)(x)))
List(1, 2, 2, 3, 3, 3) List(List(1), List(2, 2), List(3, 3, 3))

List.fill(n)(v) makes a list of n copies of v. map keeps the three inner lists; flatMap glues them.

Try it: flatten then map, or map inside
println(List(List(3), List(1, 2), List()).flatten.map(_ + 1) + " " + List(List(3), List(1, 2), List()).map(_.map(_ + 1)))
List(4, 2, 3) List(List(4), List(2, 3), List())

Same numbers. The first is flat (the empty list vanished), the second keeps the shape, including the empty inner list.

Try it: tree with subtraction

For t2 above, what is t2.reduce(_ - _)? The tree is Node(Node(1, 20), Node(29, 300)), so (1 - 20) - (29 - 300) = -19 - (-271) = 252. The run printed 252. The tree shape decides the brackets.

for ... yield with several generators: flatMap

With two generators in the same for, the result is flat: for (x <- l; y <- m) yield e = l.flatMap(x => m.map(y => e)). Each x produces the list m.map(y => e), and flatMap glues those lists in the order of x.

val l = List(1, 2, 3)
val m = List(10, 20)
println(for (x <- l; y <- m) yield x + y)
println(l.map(x => m.map(y => x + y)))
println(l.map(x => m.map(y => x + y)).flatten)
println(for (x <- l) yield for (y <- m) yield x + y)
List(11, 21, 12, 22, 13, 23)
List(List(11, 21), List(12, 22), List(13, 23))
List(11, 21, 12, 22, 13, 23)
List(List(11, 21), List(12, 22), List(13, 23))

Lines 1 and 3 are the same flat list; lines 2 and 4 are the same nested list. So: two generators in one for = flatMap + map; a for nested in the yield = map + map.

Where does the guard go? A guard filters the generator just before it. With the guard between the generators, the outer list is filtered; with the guard at the end, the inner list is filtered (even if the condition mentions only x).

println(for (x <- l; if x > 1; y <- m) yield x * y)     // l.filter(_ > 1).flatMap(x => m.map(y => x * y))
println(for (x <- l; y <- m; if x > 1) yield x * y)     // l.flatMap(x => m.filter(y => x > 1).map(y => x * y))
List(20, 40, 30, 60)
List(20, 40, 30, 60)

Three generators: the first two become flatMap, the last map: l.flatMap(x => m.flatMap(y => List(x, y).map(z => z))) (compiler output above).

Mixing List and Option. The outermost method belongs to the first generator, so the first generator decides the result type. A List first and an Option inside works: List.flatMap accepts anything iterable, and an Option counts as a collection of 0 or 1 elements.

val opt: Option[Int] = Some(5)
println(for (x <- l; y <- opt) yield x + y)                                    // l.flatMap(x => opt.map(y => x + y))
println(for (x <- l; y <- (if (x % 2 == 1) Some(x) else None)) yield y * 100)
println(for (x <- opt; y <- Some(x + 1)) yield x * y)
println(for (x <- Option(3); y <- (None: Option[Int])) yield x * y)
List(6, 7, 8)
List(100, 300)
Some(30)
None

The other way round fails. Option.flatMap requires the function to return an Option, so an Option first with a List inside does not compile:

println(for (x <- Some(1); y <- List(1, 2)) yield x + y)
error: type mismatch;
 found   : List[Int]
 required: Option[?]
The rule In one for: every generator except the last is a flatMap, the last is a map, the result is flat and has the type of the first generator. The output order is the odometer order (first generator slowest).
Trap (1) map for the outer generator gives a nested list, which is wrong for a single for. (2) flatMap for the last generator: List(4,5).flatMap(y => x * y) does not even compile, x * y is not a list. (3) Swapping which collection is outer changes the order. (4) Putting the guard on the wrong collection: a guard on x placed right after x <- l filters l, not the inner list. (5) Using the variable of the outer generator as the parameter name of an inner lambda, like l.map(x => x * y) in options where y is not in scope: those options do not compile.
On the exam "What is the desugared form?" with two generators (and maybe a guard): answer shape is A.filter(...)?.flatMap(x => B.filter(...)?.map(y => e)). Compare the declared result type: List[Int] means flat, so flatMap; List[List[Int]] or Seq[Seq[...]] means nested, so map with a yield for inside. For "which for loops are equivalent", two guards can be merged with && and moved to the end without changing the result (for pure conditions), but a yield if (...) a else None is never a filter: it yields a value for every pair.
Try it: order of pairs
println(for (a <- List(1, 2); b <- List("x", "y")) yield s"$a$b")
List(1x, 1y, 2x, 2y)

Desugared: List(1, 2).flatMap(a => List("x", "y").map(b => s"$a$b")). b changes fastest.

Try it: inner generator depends on outer
println(for (x <- List(1, 2, 3); y <- List(x, x * 10) if y != 2) yield y)
List(1, 10, 20, 3, 30)

Pairs in order: x=1: y=1, 10. x=2: y=2 (dropped by the guard), 20. x=3: y=3, 30. Desugared: List(1,2,3).flatMap(x => List(x, x*10).filter(y => y != 2).map(y => y)).

Try it: strings are collections too
println(for (s <- List("ab", "cde"); c <- s) yield c.toUpper)
List(A, B, C, D, E)

c <- s runs over the characters of the string. Two generators, so flatMap: the result is one flat list of characters.

Try it: flat or nested?

Given xss = List(List(1, 2), List(5), List(7, 8, 9)), compare for (a <- xss if a.length > 1) yield a.map(_ * 2) with xss.filter(_.length > 1).flatMap(a => a.map(_ * 2)). The for has one generator, so it is a map: List(List(2, 4), List(14, 16, 18)). The chain uses flatMap: List(2, 4, 14, 16, 18). Both outputs are from the runs above. Not equivalent.

Folds: the general idea, signatures and recursion shapes

A fold collapses a whole collection into one value by repeatedly combining an accumulator (the result so far) with the next element, using an operator you supply, starting from a start value z (often called the "zero"). Sum, product, max, count, exists, forall, map, filter, flatten: all of them are folds with a particular z and operator.

There are three folds, and they differ only in where the brackets go (the order in which the operator is applied). For List(a, b, c) and operator op:

foldlibrary signature (Scala 2.13)professor's stylebrackets
foldLeftfoldLeft[B](z: B)(op: (B, A) => B): B(List[A], B, (B,A) => B) => B((z op a) op b) op c
foldRightfoldRight[B](z: B)(op: (A, B) => B): B(List[A], B, (A,B) => B) => Ba op (b op (c op z))
foldfold[A1 >: A](z: A1)(op: (A1, A1) => A1): A1(A, (A,A) => A, List[A]) => Aany grouping, e.g. (a op b) op (c op z)

Read the operator type to recognise the fold: the accumulator B sits on the left in foldLeft and on the right in foldRight. fold has only one type: the start value, the elements and the result must all be the same type (or a common supertype). That is why fold cannot turn strings into an Int:

println(List("ab", "cde").foldLeft(0)(_ + _.length) + " " + List("ab", "cde").foldRight(0)(_.length + _))
println(List("ab", "cde").fold(0)(_ + _.length))
5 5
error: value length is not a member of Any

(The common supertype of Int and String is Any, the top type, and Any has no length.)

The slides draw the three as trees with 7 elements. Here they are with 4 elements, op at every inner point, evaluated from the bottom up:

foldLeftfoldRightfold opopopop dcbza opopopop abcdz opopop abcd

Recognising a fold from its recursion. The exam shows a recursive definition and asks which function it is. Learn the shapes:

def foldL[A, B](l: List[A], z: B, f: (B, A) => B): B = l match {
  case Nil    => z
  case h :: t => foldL(t, f(z, h), f)        // combine FIRST, then recurse with the new accumulator
}
def foldR[A, B](l: List[A], z: B, f: (A, B) => B): B = l match {
  case Nil    => z
  case h :: t => f(h, foldR(t, z, f))        // recurse FIRST, then combine the head with that result
}
println(foldL[Int, Int](List(1, 2, 3), 0, _ - _) + " " + foldR[Int, Int](List(1, 2, 3), 0, _ - _))
-6 2
recursive casebase casereturnsname
rec(t, f(z, h))zBfoldLeft (tail recursive)
f(h, rec(t))zBfoldRight
f(h) :: rec(t)NilList[B]map
f(h) ++ rec(t)NilList[B]flatMap
if (p(h)) h :: rec(t) else rec(t)NilList[A]filter
p(h) || rec(t)falseBooleanexists
p(h) && rec(t)trueBooleanforall

Relatives: scanLeft and unfold. A scan is a foldLeft that writes down every intermediate accumulator. The library scanLeft includes the start value; a hand written one in an exam may not, so read its code. An unfold is the opposite of a fold: it starts from one seed value and produces a list. A step function returns Some((element, nextSeed)) to emit an element and continue, or None to stop.

println(List(1, 2, 3, 4).scanLeft(0)(_ + _) + " " + List(1, 2, 3, 4).scanLeft(10)(_ - _))
def unfold[A, B](seed: B)(f: B => Option[(A, B)]): List[A] = f(seed) match {
  case None            => Nil
  case Some((a, next)) => a :: unfold(next)(f)
}
println(unfold(1)(n => if (n > 20) None else Some((n, n * 3))))
List(0, 1, 3, 6, 10) List(10, 9, 7, 4, 0)
List(1, 3, 9)
The rule foldLeft: accumulator first, (B, A) => B, brackets pile up on the left, z meets the first element. foldRight: element first, (A, B) => B, brackets pile up on the right, z meets the last element. fold: one type A, (A, A) => A, grouping not specified.
Trap Signature questions: (List[A], B, (A,B) => B) => B is foldRight, not foldLeft and not fold (fold has no separate B). (A, (A,A) => A, List[A]) => A is plain fold. For scans: do not reset the accumulator at the boundary of former inner lists after a flatten, and check whether the start value is emitted. For unfolds: emit the first component, continue with the second, stop exactly when None comes back.
On the exam "Which higher order function is this?" (signature or recursion): use the two tables above. "Which functions implement foldLeft?": a var updated as res = f(res, e) in a for or while loop, and the tail recursive rec(f(z, h))(tail), are correct. f(l.head, rec(l.tail)) is the foldRight shape, and with f: (B, A) => B it does not even type check (error: type mismatch; found: A, required: B).
Try it: an unfold
println(unfold(10)(n => if (n <= 0) None else Some((n % 3, n - 4))))
List(1, 0, 2)

Seed 10: emit 10 % 3 = 1, next 6. Seed 6: emit 0, next 2. Seed 2: emit 2, next -2. Seed -2: None, stop.

Try it: a scan with subtraction

List(1, 2, 3, 4).scanLeft(10)(_ - _): start 10 (emitted), then 10-1 = 9, 9-2 = 7, 7-3 = 4, 4-4 = 0. Output above: List(10, 9, 7, 4, 0). A hand written scan that appends only after each step would give List(9, 7, 4, 0).

foldLeft

l.foldLeft(z)(op) walks the list from left to right, carrying an accumulator. It starts with acc = z, and for each element x does acc = op(acc, x). At the end it returns acc. That is exactly this loop:

def fl[A, B](z: B)(op: (B, A) => B)(l: List[A]): B = {
  var acc = z
  for (x <- l) acc = op(acc, x)
  acc
}

Paper method: the accumulator table. Write z, then one row per element with the new accumulator. With a print in the operator you can watch Scala do the same:

def tl(acc: Int, x: Int) = { print(s"op($acc,$x) "); acc - x }
val k = List(1, 2, 3).foldLeft(0)(tl); println("=> " + k)
op(0,1) op(-1,2) op(-3,3) => -6
stepacc beforexacc after = acc - x
start0
101-1
2-12-3
3-33-6

As brackets: ((0 - 1) - 2) - 3 = -6. String building shows the bracket structure directly:

println(List("a", "b", "c").foldLeft("z")(_ + _))
println(List("a", "b", "c").foldLeft("z")((acc, x) => s"($acc $x)"))
zabc
(((z a) b) c)

The accumulator can be any type, including a list, a tuple or a function. Building a list with foldLeft by prepending (::) reverses it; appending (:+) keeps the order:

println(List(1, 2, 3).foldLeft(List[Int]())((acc, x) => x :: acc))
println(List(1, 2, 3).foldLeft(List[Int]())((acc, x) => acc :+ (x * 10)))
List(3, 2, 1)
List(10, 20, 30)

Implementing the other functions with foldLeft. Pick the start value that is correct for the empty list, then write the step "old answer + one more element gives new answer":

def myCount[A](xs: List[A])(p: A => Boolean)  = xs.foldLeft(0)((c, x) => if (p(x)) c + 1 else c)
def myExists[A](xs: List[A])(p: A => Boolean) = xs.foldLeft(false)((b, x) => b || p(x))
def myForall[A](xs: List[A])(p: A => Boolean) = xs.foldLeft(true)((b, x) => b && p(x))
def myMap[A, B](xs: List[A])(f: A => B)       = xs.foldLeft(List[B]())((acc, x) => acc :+ f(x))
def myFilter[A](xs: List[A])(p: A => Boolean) = xs.foldLeft(List[A]())((acc, x) => if (p(x)) acc :+ x else acc)
def myFlatten[A](xs: List[List[A]])           = xs.foldLeft(List[A]())(_ ++ _)
val ns = List(4, 7, 2, 9)
println(myCount(ns)(_ > 3) + " " + myExists(ns)(_ > 8) + " " + myForall(ns)(_ > 1) + " " + myMap(ns)(_ + 1) + " " +
        myFilter(ns)(_ % 2 == 0) + " " + myFlatten(List(List(1), List(2, 3))))
println(myExists(List[Int]())(_ > 0) + " " + myForall(List[Int]())(_ > 0))
3 true true List(5, 8, 3, 10) List(4, 2) List(1, 2, 3)
false true

The operator is fixed before the fold starts. If you pass an overloaded method name like wins to foldLeft, Scala picks one overload at compile time from the static types ((Roshambo, Roshambo) => Roshambo), and that one is used for every step, whatever the runtime classes are. If instead you pass _.wins(_), each step is a method call on the accumulator, and dynamic dispatch picks the override of the accumulator's runtime class. (Overloading and dispatch are taught in the polymorphism lesson; for the fold, just trace step by step with the right method.)

The rule foldLeft = a loop with acc = op(acc, x), left to right, z combined with the first element. Brackets: ((z op a) op b) op c. The accumulator is the first parameter of op.
Trap (1) Evaluating from the wrong end: List(5, 10).foldLeft(0)(_ - _) is (0 - 5) - 10 = -15, while the foldRight value 5 - (10 - 0) is -5 (both printed by a run). (2) Swapping to element - acc. (3) Forgetting the start value (foldLeft(40) starts at 40). (4) Using :: with the arguments in the wrong order: foldLeft(List[Int]())(_ :: _) does not compile (error: value :: is not a member of Int), because the first _ is the list accumulator.
On the exam Value questions (foldLeft(0)(_ - _), with a tuple accumulator, with a "winner" function, after a flatMap): use the accumulator table. "Which implement foldLeft": loop with var, while loop, tail recursion passing f(z, head) on. Not: foldRight, recursion of the shape f(head, rec(tail)), or a divide and conquer version (that needs A = B, it fails to type check for a general foldLeft: found: A, required: B at l.head).
Try it: a non-associative step
println(List(4, 1, 3).foldLeft(2)((a, x) => a * x - 1))
17

acc = 2. x = 4: 2*4-1 = 7. x = 1: 7*1-1 = 6. x = 3: 6*3-1 = 17.

Try it: digits
println(List(2, 3, 4).foldLeft(1)((a, x) => a * 10 + x))
1234

1, then 12, 123, 1234. The start value ends up as the leading digit, because it is combined first.

Try it: rock, paper, scissors
sealed trait Hand
case object R extends Hand
case object P extends Hand
case object S extends Hand
def beats(a: Hand, b: Hand) = (a, b) match {
  case (P, R) | (R, S) | (S, P) => true
  case _ => false
}
def winner(a: Hand, b: Hand): Hand = if (beats(a, b)) a else b
println(List(P, S, R).foldLeft(P: Hand)(winner))
R

Note winner returns the right argument on a tie. acc = P. winner(P, P): tie, P. winner(P, S): P does not beat S, so S. winner(S, R): S does not beat R, so R.

Try it: odd and even
println((1 to 4).foldLeft(List[Int]())((acc, x) => if (x % 2 == 0) x :: acc else acc :+ x))
List(4, 2, 1, 3)

[] then 1 appended: [1]. 2 prepended: [2, 1]. 3 appended: [2, 1, 3]. 4 prepended: [4, 2, 1, 3].

foldRight

l.foldRight(z)(op) puts the brackets on the right: a op (b op (c op z)). The start value z is combined with the last element first, and the operator takes the element first, accumulator second: op: (A, B) => B. The elements keep their left to right positions in the expression; only the grouping changes. So foldRight does not reverse the list.

Paper method: write the brackets, then evaluate from the innermost bracket outwards. The innermost bracket is (last op z).

def tr(x: Int, acc: Int) = { print(s"op($x,$acc) "); x - acc }
val k = List(1, 2, 3).foldRight(0)(tr); println("=> " + k)
op(3,0) op(2,3) op(1,-1) => 2
stepelement xacc (result of everything to the right)x - acc
130 (z)3
223-1
31-12

As brackets: 1 - (2 - (3 - 0)) = 1 - (2 - 3) = 1 - (-1) = 2. Compare foldLeft on the same list: -6. Side by side with strings:

println(List("a", "b", "c").foldLeft("z")(_ + _) + " " + List("a", "b", "c").foldRight("z")(_ + _))
println(List("a", "b", "c").foldLeft("z")((acc, x) => s"($acc $x)"))
println(List("a", "b", "c").foldRight("z")((x, acc) => s"($x $acc)"))
zabc abcz
(((z a) b) c)
(a (b (c z)))

Building a list with foldRight and :: keeps the order (it copies the list), while foldLeft with :: reverses:

println(List(1, 2, 3).foldLeft(List[Int]())((acc, x) => x :: acc) + " " + List(1, 2, 3).foldRight(List[Int]())((x, acc) => x :: acc))
List(3, 2, 1) List(1, 2, 3)

Folding functions. The accumulator can be a function. With andThen(f, g)(x) = g(f(x)) (first f, then g), foldLeft puts z at the start of the pipeline and foldRight at the end:

val fs: List[Int => Int] = List(_ * 2, _ + 3, _ * 4)
def andThen[A, B, C](f: A => B, g: B => C)(x: A): C = g(f(x))
val minus1: Int => Int = _ - 1
val left  = fs.foldLeft[Int => Int](minus1)(andThen(_, _))   // ((minus1 then *2) then +3) then *4
val right = fs.foldRight[Int => Int](minus1)(andThen(_, _))  // *2 then (+3 then (*4 then minus1))
println(left(1) + " " + right(1))
12 19

left(1): 1-1 = 0, *2 = 0, +3 = 3, *4 = 12. right(1): 1*2 = 2, +3 = 5, *4 = 20, -1 = 19.

The rule foldRight: a op (b op (c op z)), operator (element, acc), z meets the last element, evaluation starts at the right end. Element order is not reversed; only the grouping and where z sits change.
Trap (1) Feeding the elements left to right as if it were foldLeft. (2) Mixing up which argument is the accumulator; in foldRight(z)(_ - _) the first _ is the element. (3) Assuming foldLeft and foldRight always agree: they agree for an associative operator with an identity start value (next sections), not for -, a "winner" function, or a non-identity start. (4) When the step appends (acc ++ List(e, e+1)) inside a foldRight, the chunks come out in reverse order: List(1,3).foldRight(List[Int]())((e, a) => a ++ List(e, e+1)) is List(3, 4, 1, 2).
On the exam Typical: a pair (l.foldLeft(z)(op), l.foldRight(z)(op)) and you pick the pair. Do two separate traces: foldLeft with the accumulator table from the left, foldRight with the bracket method from the right. Then check: is op associative and z an identity? If not, expect two different answers, and be suspicious of options where both are equal.
Try it: the same step, both directions
println(List(4, 1, 3).foldRight(2)((x, a) => a * x - 1))
15

Start from the right with acc = 2. x = 3: 2*3-1 = 5. x = 1: 5*1-1 = 4. x = 4: 4*4-1 = 15. The foldLeft version gave 17.

Try it: tuple accumulator
def stepL(acc: (Int, Int), x: Int): (Int, Int) = (acc._1 + x, acc._2 * 2 - x)
def stepR(x: Int, acc: (Int, Int)): (Int, Int) = (acc._1 + x, acc._2 * 2 - x)
println(List(1, 2, 3).foldLeft((0, 1))(stepL) + " " + List(1, 2, 3).foldRight((0, 1))(stepR))
(6,-3) (6,-9)

The first component is a sum, so both directions give 6. The second is not associative. Left: (0,1), x=1: (1, 2-1=1), x=2: (3, 2-2=0), x=3: (6, 0-3=-3). Right: x=3 first: (3, 2-3=-1), x=2: (5, -2-2=-4), x=1: (6, -8-1=-9).

Try it: division
println(List(8, 4, 2).foldLeft(64)(_ / _) + " " + List(8, 4, 2).foldRight(1)(_ / _))
1 4

Left: ((64/8)/4)/2 = (8/4)/2 = 2/2 = 1. Right: 8/(4/(2/1)) = 8/(4/2) = 8/2 = 4.

Try it: rock, paper, scissors from the right

With winner from the foldLeft section, List(P, S, R).foldRight(P: Hand)(winner) is winner(P, winner(S, winner(R, P))). Inner: R does not beat P, so P. Then S beats P, so S. Then P does not beat S, so S. The run printed S; foldLeft gave R.

fold (order not specified)

l.fold(z)(op) also combines all elements, but it makes no promise about the grouping. It may group like foldLeft, like foldRight, or like a balanced tree (the slide picture), and it may use z more than once. Why would you want that? The slides give two reasons: it is easier to read (it says "the tree does not matter"), and the library can use that freedom to optimise, in particular to run in parallel: split the list into chunks, fold each chunk on a different processor starting from z, then combine the chunk results. That is the idea behind l.map(f).reduce(op) and MapReduce.

The price: the result is only well defined if the grouping and the number of zs cannot matter. The slide's fold requirement:

  • z must be an identity element for op: a op z == z op a == a for all a.
  • op must be associative: a op (b op c) == (a op b) op c for all a, b, c.

With both, every grouping gives the same answer, so fold, foldLeft and foldRight all agree. The slide examples are all of this kind:

def sum(x: List[Int])         = x.fold(0)(_ + _)
def product(x: List[Int])     = x.fold(1)(_ * _)
def maxL(x: List[Int])        = x.fold(Int.MinValue)(_ max _)
def minL(x: List[Int])        = x.fold(Int.MaxValue)(_ min _)
def all(x: List[Boolean])     = x.fold(true)(_ && _)
def any(x: List[Boolean])     = x.fold(false)(_ || _)

What a parallel fold does, simulated so you can see it. Split into chunks, foldLeft each chunk from z, then combine the chunk results with op:

def chunkedFold[A](xs: List[A], chunks: Int)(z: A)(op: (A, A) => A): A = {
  val size = math.max(1, (xs.length + chunks - 1) / chunks)
  xs.grouped(size).toList.map(_.foldLeft(z)(op)).reduce(op)
}
val six = List(1, 2, 3, 4, 5, 6)
for (c <- List(1, 2, 3))
  println(s"$c chunks: sum0=" + chunkedFold(six, c)(0)(_ + _) + " sum1=" + chunkedFold(six, c)(1)(_ + _) +
          " minus0=" + chunkedFold(six, c)(0)(_ - _))
1 chunks: sum0=21 sum1=22 minus0=-21
2 chunks: sum0=21 sum1=23 minus0=9
3 chunks: sum0=21 sum1=24 minus0=15

With (0, +), a monoid, the answer is always 21. With start 1, which is not an identity for +, each chunk adds an extra 1. With -, which is not associative, every split gives a different number.

On an ordinary List, Scala 2.13 happens to run fold as a foldLeft:

println(List(1, 2, 3).fold(0)(_ - _) + " " + List(1, 2, 3).foldLeft(0)(_ - _) + " " + List(1, 2, 3).foldRight(0)(_ - _))
-6 -6 2

Do not build on that. The contract of fold says the order is unspecified, so with a non-monoid you must treat the result as "not determined". Exam questions are written so that this does not matter: they compare fold with both foldLeft and foldRight, or ask which implementations are correct given the laws.

The rule Use fold(z)(op) only when op is associative and z is its identity (together: a monoid). Then fold == foldLeft == foldRight, for every list. Commutativity (a op b == b op a) is not required.
Trap (1) Thinking fold is only for parallel collections; it works on any collection. (2) Using fold to change type (fold(0)(_ + _.length) on strings): fold has one type parameter, use foldLeft. (3) Rejecting a divide and conquer implementation of fold because it groups "strangely": given associativity and identity, any grouping is correct, including f(fold(firstHalf), fold(secondHalf)), a var loop, recursion, foldLeft and foldRight.
On the exam (a) "Which higher order functions can be x in List(1,2,3,4).x(1)(_ * _) so that the result is 24?" All three folds: (1, *) is a monoid. (b) "For which z and f does fold == foldLeft && foldLeft == foldRight always hold?" Check both laws for each option. (c) "The signature (A, (A,A) => A, List[A]) => A belongs to?" fold. (d) "Which implementations of fold are correct, assuming f associative and z a unit?" All the ones that type check and handle the empty list by returning z.
Try it: which z work for max?
for (c <- List(1, 2, 3))
  println(s"$c chunks: max0=" + chunkedFold(List(-4, -2, -7, -1), c)(0)(_ max _) +
          " maxMin=" + chunkedFold(List(-4, -2, -7, -1), c)(Int.MinValue)(_ max _))
1 chunks: max0=0 maxMin=-1
2 chunks: max0=0 maxMin=-1
3 chunks: max0=0 maxMin=-1

With z = 0 every split agrees, but on 0. 0 is consistent here but wrong: it is not an identity for max because 0 max -5 = 0, not -5. Only the smallest possible value works as identity.

Try it: string concatenation with a non-empty start
val letters = List("a", "b", "c", "d", "e", "f")
for (c <- List(1, 2, 3))
  println(s"$c chunks: cat=" + chunkedFold(letters, c)("")(_ + _) + " x=" + chunkedFold(letters, c)("x")(_ + _))
println(List("a","b","c").foldLeft("x")(_ + _) + " " + List("a","b","c").foldRight("x")(_ + _))
1 chunks: cat=abcdef x=xabcdef
2 chunks: cat=abcdef x=xabcxdef
3 chunks: cat=abcdef x=xabxcdxef
xabc abcx

With "" every split gives abcdef. With "x" you get one extra x per chunk, and sequentially foldLeft puts it in front while foldRight puts it at the end. Concatenation is associative, but "x" is not an identity.

Associativity

An operator ⊕ is associative if the grouping of a chain does not matter: a ⊕ (b ⊕ c) == (a ⊕ b) ⊕ c for all a, b, c. The order of the operands stays the same; only the brackets move. Because foldLeft, foldRight and fold differ only in brackets (look at the trees), associativity is what makes them agree, provided the start value also behaves (next section).

operatorassociative?check
+, * on Intyes(1+2)+3 = 1+(2+3)
max, minyeslargest of three, however grouped
&&, ||yes
++ on lists, + on stringsyes("a"+"b")+"c" = "a"+("b"+"c")
function composition andThenyes(f andThen g) andThen h = f andThen (g andThen h)
-, /no(1-2)-3 = -4 but 1-(2-3) = 2
average (a+b)/2no
"winner" in rock, paper, scissorsnothe result depends on who meets whom first

Associative is not commutative. Commutative means you may swap operands: a ⊕ b == b ⊕ a. String and list concatenation are associative but not commutative ("ab" != "ba"), and they still work with fold, because no fold ever swaps operands; folds only regroup.

println(List(1.0, 2.0, 3.0).foldLeft(0.0)((a, b) => (a + b) / 2) + " " + List(1.0, 2.0, 3.0).foldRight(0.0)((a, b) => (a + b) / 2))
2.125 1.375
The rule Associative = brackets may move, operands stay in place. That is exactly the freedom foldLeft/foldRight/fold use, so associativity (plus an identity start) makes them all equal.
Trap (1) Accepting - because 0 "looks neutral". (2) Thinking concatenation fails because it is not commutative. (3) Thinking a foldRight over ++ reverses the result: it does not, it only regroups.
On the exam To test associativity quickly, try three small distinct values such as 1, 2, 3 in both groupings. One mismatch proves "not associative". For function folds, composition is associative, so the difference between foldLeft and foldRight comes only from where the start function lands (beginning or end of the pipeline).
Try it: is it associative?

Is op(a, b) = a * 10 + b associative? Check with 2, 3, 4: op(op(2,3),4) = 23*10+4 = 234; op(2, op(3,4)) = 2*10 + 34 = 54. No. That is why List(2,3,4).foldLeft(1)((a, x) => a * 10 + x) gave 1234, while List(2,3,4).foldRight(1)((x, a) => a * 10 + x) gave 1432 (both from the run).

Try it: composition with the identity function
val id: Int => Int = x => x
val left2  = fs.foldLeft[Int => Int](id)(andThen(_, _))
val right2 = fs.foldRight[Int => Int](id)(andThen(_, _))
println(left2(1) + " " + right2(1))
20 20

Composition is associative and id is its identity, so both folds build the same pipeline *2, +3, *4: (1*2+3)*4 = 20. With minus1 as start they gave 12 and 19.

Identity element

An identity element (also called unit or neutral element) for ⊕ is a value 𝟘 that changes nothing: a ⊕ 𝟘 == 𝟘 ⊕ a == a for every a. It must work on both sides. The start value of a fold should be an identity, because foldLeft puts z on the left of the first element, foldRight on the right of the last element, and a parallel fold may insert it many times. Only an identity is harmless in all those places.

operatoridentitynot an identity
+01 (adds 1 per use)
*10 (everything becomes 0)
maxInt.MinValue (the slide's −∞)0 (0 max -5 = 0)
minInt.MaxValue (∞)
&&truefalse (always false)
||falsetrue (always true)
++ / string +List() / """bla"
andThenx => x_ + 1
-none: 0 works only on the right (a - 0 = a but 0 - a = -a)
def foldEquiv[A](z: A, f: (A, A) => A, xs: List[A]) =
  xs.fold(z)(f) == xs.foldLeft(z)(f) && xs.foldLeft(z)(f) == xs.foldRight(z)(f)
println(foldEquiv[Int](0, _ + _, six) + " " + foldEquiv[Int](0, _ - _, six) + " " +
        foldEquiv[String]("", _ ++ _, List("a","b")) + " " + foldEquiv[String]("bla", _ ++ _, List("a","b")) + " " +
        foldEquiv[Int](1, _ * _, six) + " " + foldEquiv[Int](0, _ max _, List(-3, -1)) + " " +
        foldEquiv[Int](Int.MinValue, _ max _, List(-3, -1)) + " " + foldEquiv[Int](0, _ - _, List()) + " " +
        foldEquiv[Int](0, _ - _, List(5)))
true false true false true true true true false

Two results need a comment. (0, max) on List(-3, -1) gives true: all three folds agree on the wrong answer 0 (max is associative and commutative, so 0 just sits in the comparison everywhere). Agreement is not the same as correctness; for "is this a correct max" the answer is still no. And (0, -) passes on the empty list (all give 0) but fails on List(5): foldLeft gives 0 - 5 = -5, foldRight gives 5 - 0 = 5. One element is enough to expose a one-sided identity.

The rule z is an identity if z ⊕ a == a and a ⊕ z == a for every a. Test with one element list: foldLeft gives z ⊕ a, foldRight gives a ⊕ z, both must be a.
Trap Picking a start that works for the data in front of you (0 for max over positive numbers) and concluding it is an identity. The definition says "for all a". Also, a one-sided identity (0 for -) is not enough.
On the exam In "for which A, z, f does it always return true" questions, go through each option: associative? identity on both sides? Both yes: correct. Either no: find the small list that breaks it (one element for a bad z, three elements for a non-associative op).
Try it: product with the wrong start
println(List(2, 3).fold(0)(_ * _) + " " + List(2, 3).fold(1)(_ * _))
0 6

0 is not the identity for *; it absorbs everything: (0 * 2) * 3 = 0. Every grouping gives 0, so the folds agree, but the "product" is wrong. The identity is 1, giving 6, which is why the slide's product starts at 1.

Try it: why sum1 grows with chunks

In the parallel simulation, sum1 was 22, 23, 24 for 1, 2, 3 chunks. Each chunk starts from z = 1, so the result is 21 + (number of chunks). An identity contributes nothing no matter how often it is used; 1 contributes 1 each time.

Monoid

The slide's "math aside": in abstract algebra a monoid is a set A together with an associative operator ⊕ and an identity element 𝟘, such that

  • associativity: ∀ a b c, a ⊕ (b ⊕ c) = (a ⊕ b) ⊕ c
  • identity: ∀ a, a ⊕ 𝟘 = 𝟘 ⊕ a = a

"... exactly the arguments and requirements for fold!" A call l.fold(𝟘)(⊕) is well defined precisely when (A, ⊕, 𝟘) is a monoid. So when the exam asks whether fold is safe, or whether two folds always agree, it is asking: is this a monoid?

set𝟘⊕
Int0+
Int1×
Int ∪ {−∞}−∞ (Int.MinValue)max
Int ∪ {∞}∞ (Int.MaxValue)min
{⊥, ⊤} (Boolean)⊤ (true)∧ (&&)
{⊥, ⊤} (Boolean)⊥ (false)∨ (||)
String""concatenation
List[A]List()++
functions A => Ax => xandThen

The first six rows are on the slide; the last three follow from the same two laws, and the exam uses all of them. Not monoids: (Int, -, 0), (Int, /, 1), (String, ++, "bla"), (Int, max, 0), averaging.

println(List(1, 2, 3, 4).fold(0)(_ + _) + " " + List(1, 2, 3, 4).fold(1)(_ * _) + " " + List(3, 9, 2).fold(Int.MinValue)(_ max _) + " " +
        List(true, false).fold(true)(_ && _) + " " + List(true, false).fold(false)(_ || _))
println(List("ab", "c", "de").fold("")(_ + _) + " " + List(List(1), List(2, 3)).fold(List[Int]())(_ ++ _))
10 24 9 false true
abcde List(1, 2, 3)
The rule Monoid = (set, associative operator, identity). Monoid means fold is safe and fold == foldLeft == foldRight. No commutativity needed.
Trap A set and an operator are not enough; the identity is part of the monoid. (String, ++) with "bla" as start is not the monoid (String, ++, "").
On the exam Any question with fold or with "always equal" between folds: name the triple (set, operator, start), and check it against the table. If it is in the table, all folds agree; if not, trace each fold separately.
Try it: monoid or not?

(a) (Boolean, ||, true): associative yes, but false || true = true, not false, so true is not an identity. Not a monoid. (b) (List[Int], ++, List()): monoid (slide-style flatten, the run printed List(1, 2, 3)). (c) (Int, min, Int.MaxValue): monoid. (d) (Int, *, 0): 0 is not an identity. Not a monoid.

Try it: three folds, one answer?

For List(1,2,3,4), are foldLeft(1)(_ * _), foldRight(1)(_ * _) and fold(1)(_ * _) all 24? Yes: (Int, *, 1) is a monoid. The run above printed 24 for fold(1)(_ * _), and foldEquiv[Int](1, _ * _, six) printed true.

Recap: Week 4 higher order functions in one screen

callresultremember
l.exists(p)Booleansome passes; empty: false; stops at first true
l.forall(p)Booleannone fails; empty: true; stops at first false
l.map(f)List[B]same length and order; never flattens
l.foreach(f)()side effects only
l.filter(p)List[A]tests every element
l.takeWhile(p)List[A]prefix until first failure
l.dropWhile(p)List[A]from first failure on, keeps everything after
l.count(p)Int= filter(p).length; not map(p).length
l.flattenList[A]from List[List[A]], removes one layer
l.flatMap(f)List[B]f: A => List[B]; = map then flatten
l.foldLeft(z)(op)Bop(acc, x); ((z⊕a)⊕b)⊕c
l.foldRight(z)(op)Bop(x, acc); a⊕(b⊕(c⊕z))
l.fold(z)(op)Aneeds a monoid; then equals both

Desugaring

  • for (x <- a) body = a.foreach(x => body); for (x <- a; y <- b) body = a.foreach(x => b.foreach(y => body)).
  • for (x <- a if p) yield e = a.withFilter(p).map(x => e), same result as a.filter(p).map(x => e).
  • for (x <- a; y <- b) yield e = a.flatMap(x => b.map(y => e)): flat.
  • for (x <- a) yield for (y <- b) yield e = a.map(x => b.map(y => e)): nested.
  • Guard filters the generator right before it. Result type = type of the first generator. List then Option works; Option then List does not compile.
  • Output order: odometer, first generator slowest.

Fold checklist

  • foldLeft: accumulator table, left to right, z meets the first element.
  • foldRight: write the brackets, start at (last op z), work outwards. No reversal of elements.
  • Same answer from all folds when (set, op, z) is a monoid: (Int,+,0), (Int,*,1), (max, MinValue), (min, MaxValue), (&&, true), (||, false), (++, empty), (andThen, identity).
  • Not a monoid: -, /, average, "winner", or any good operator with a wrong start ("bla", 0 for max, 1 for +).
  • Commutativity is never needed.
  • Recognise by signature: (B,A)=>B foldLeft, (A,B)=>B foldRight, (A,A)=>A fold, A=>List[B] flatMap, List[List[A]]=>List[A] flatten.
  • Recognise by recursion: rec(t, f(z,h)) foldLeft, f(h, rec(t)) foldRight, f(h) :: rec(t) map, p(h) && rec(t) with base true forall, || with base false exists.

Equivalence questions: test each candidate on the empty list, a one element list that passes, and a one element list that fails. Check the result type (a for ... yield is a collection, never a Boolean or a sum).

Week 5: Pattern matching, tail recursion and lists

Week 5 is about taking data apart by its shape (pattern matching), about recursion that does not eat the stack (tail recursion), and about the list, the data structure that is built entirely from those two ideas. On the exam this week mostly shows up as "what is the value of this simplify/flatMap/reverse": code where you must run a match by hand, one case at a time, without skipping steps.

How a match expression runs

A match expression looks at one value and picks the first case whose pattern fits that value. The value in front of match is called the scrutinee. A pattern is a description of a shape, like "a Some with anything inside" or "a Plus whose left child is Const(0)".

x match {
  case pattern1 => result1
  case pattern2 => result2
  case _        => result3   // _ fits everything
}

The machine does exactly this:

  1. Try the cases from top to bottom.
  2. For each case, check if the pattern fits the value. If the pattern has names in it (like l, r), those names get bound to the matching parts.
  3. The first case that fits wins. Its body is evaluated and that is the value of the whole match. The other cases are never looked at again.
  4. If no case fits, the program crashes at runtime with a scala.MatchError.

The order is everything. Scala does not look for the "best" or "most specific" case: it takes the first one that fits.

def first(x: Option[Option[Int]]) = x match {
  case Some(Some(9)) => "nine inside"
  case Some(_)       => "some"
  case _             => "nope"
}
def second(x: Option[Option[Int]]) = x match {
  case Some(_)       => "some"         // fits every Some
  case Some(Some(9)) => "nine inside"  // can never be reached
  case _             => "nope"
}
println(first(Some(Some(9))) + " " + first(Some(Some(3))) + " " + first(Some(None)) + " " + first(None))
println(second(Some(Some(9))))
warning: unreachable code
  case Some(Some(9)) => "nine inside"
nine inside some some nope
some

Note: an unreachable case is only a warning. The program still compiles and runs.

The paper method for recursive simplifiers

Most exam questions on this topic give you an expression tree (Const, Var, Plus, ...) and a recursive simplify. Small details in the code change the answer completely, so you must trace, not guess. Use a call table: one row per call of simplify.

  1. Write the node of the current call.
  2. Go down the cases. For each case, compare the pattern with the node at this level only: is the constructor the same? Are constants (like Const(0) or "+") equal? Names and _ fit anything.
  3. At the first case that fits, write which names are bound to what.
  4. Write the body with those values filled in. Only calls that are literally in the body happen. If the body says r, you get r as it is, unsimplified. If it says simplify(r), you open a new row.
  5. When the body builds a new node, like Plus(simplify(l), simplify(r)), that new node is not matched again, unless the code explicitly calls simplify on the result (for example if (res == this) this else res.simplify).

Here are four simplifiers with one rule each, "0 + r becomes r". They differ only in where the recursive calls are.

sealed abstract class Exp
case class Const(n: Int) extends Exp
case class Var(s: String) extends Exp
case class Plus(l: Exp, r: Exp) extends Exp

// A: rule result returned as is
def sA(e: Exp): Exp = e match {
  case Plus(Const(0), r) => r
  case Plus(l, r)        => Plus(sA(l), sA(r))
  case other             => other
}
// B: rule result simplified again
def sB(e: Exp): Exp = e match {
  case Plus(Const(0), r) => sB(r)
  case Plus(l, r)        => Plus(sB(l), sB(r))
  case other             => other
}
// C: children first, then match on the simplified children
def sC(e: Exp): Exp = e match {
  case Plus(l, r) => (sC(l), sC(r)) match {
    case (Const(0), rs) => rs
    case (ls, rs)       => Plus(ls, rs)
  }
  case other => other
}
// D: version A repeated until nothing changes (a "fixed point")
def sD(e: Exp): Exp = {
  val res = sA(e)
  if (res == e) e else sD(res)
}
val e1 = Plus(Const(0), Plus(Const(0), Var("x")))   // 0 + (0 + x)
val e2 = Plus(Plus(Const(0), Const(0)), Var("x"))   // (0 + 0) + x
A: Plus(Const(0),Var(x))   Plus(Const(0),Var(x))
B: Var(x)   Plus(Const(0),Var(x))
C: Var(x)   Var(x)
D: Var(x)   Var(x)

Trace of sA(e2), the one most people get wrong:

callnodefirst case that fitsresult
1Plus(Plus(Const(0),Const(0)), Var(x))case 1? left child is a Plus, not Const(0): no. Case 2: yes, l = Plus(Const(0),Const(0)), r = Var(x)Plus(sA(l), sA(r)), wait for calls 2 and 3
2Plus(Const(0),Const(0))case 1, r = Const(0)Const(0)
3Var(x)case 3Var(x)
1build Plus(Const(0), Var(x)). This new node has a zero on the left, but nobody matches it again.Plus(Const(0),Var(x))

Version C gets Var(x) because it simplifies the children before it looks for the rule, so the rule sees (Const(0), Var(x)). Version D gets there because it repeats the whole pass until the result stops changing.

The rule Cases are tried top to bottom and the first fit wins. A pattern only looks at the exact node it is given (plus the sub-patterns you wrote), never deeper. Only the recursive calls written in the body happen, and a freshly built node is not matched again unless the code asks for it.
Trap Four classic mistakes: (1) thinking the most specific case wins (it is order); (2) "doing the obvious simplification" that no rule performs, for example removing a zero on the right when only Plus(Const(0), r) exists, or adding Const(1) + Const(4) when they are not siblings in the tree; (3) simplifying r when the body just returns r; (4) doing an extra pass at the end, or forgetting the extra pass that if (res != this) res.simplify does.
On the exam "What is the value of simplify(...)?" Draw the input as a tree first. Then make the call table. Before you start, read every case body and mark: does it return the bound part raw, or call simplify on it? Does it simplify the children before or after the rule? Is there a fixed-point loop (res == this)? Is there a rotation like Plus(Plus(a,b),c) => Plus(a,Plus(b,c)) applied once or until stable? Those marks decide the answer.
Try it: what does this print?
case class Times(l: Exp, r: Exp) extends Exp
def sT(e: Exp): Exp = e match {
  case Times(Const(1), r) => sT(r)
  case Times(l, Const(1)) => l
  case Times(l, r)        => Times(sT(l), sT(r))
  case other              => other
}
println(sT(Times(Const(1), Times(Times(Var("a"), Const(1)), Const(1)))))
Times(Var(a),Const(1))

Call 1: Times(Const(1), ...) fits case 1, so we compute sT(Times(Times(a,1), 1)). Call 2: case 1? left is a Times: no. Case 2: right is Const(1): yes, l = Times(a,1). The body is just l, not sT(l), so Times(Var(a),Const(1)) comes back untouched.

Try it: what does sA return here?
println(sA(Plus(Plus(Const(0), Var("x")), Plus(Const(0), Const(0)))))
Plus(Var(x),Const(0))

Root: left child is a Plus, so case 2. Left: sA(Plus(Const(0), Var(x))) fits case 1 and gives Var(x). Right: sA(Plus(Const(0), Const(0))) fits case 1 and gives Const(0). Rebuild: Plus(Var(x), Const(0)). There is no rule for a zero on the right, and the rebuilt node is not checked anyway.

Try it: the fixed-point version
println(sD(Plus(Plus(Const(0), Const(0)), Plus(Const(0), Var("y")))))
Var(y)

Pass 1 (sA): root is case 2, left child becomes Const(0), right child becomes Var(y), result Plus(Const(0), Var(y)). Different from the input, so pass 2: case 1 gives Var(y). Different again, so pass 3: sA(Var(y)) is Var(y), equal, stop.

Try it: one rotation with subtraction
sealed abstract class T
case class Leaf(n: Int) extends T
case class Op(l: T, f: (Int, Int) => Int, r: T) extends T
def ev(t: T): Int = t match { case Leaf(n) => n; case Op(l, f, r) => f(ev(l), ev(r)) }
def rotT(t: T): T = t match { case Op(Op(x, f, y), g, z) => Op(x, f, Op(y, g, z)); case o => o }
val minus = (a: Int, b: Int) => a - b
val t = Op(Op(Leaf(20), minus, Leaf(5)), minus, Leaf(3))
println(ev(t) + " " + ev(rotT(t)))
12 18

t is (20 - 5) - 3 = 12. The rotation turns it into 20 - (5 - 3) = 18. Subtraction is not associative, so moving brackets changes the value. The rotation runs once only: there is no recursive call in it.

match vs switch

If you know switch from C or Java: match is the general version of it. The professor lists five differences:

switch (C/Java)match (Scala)
Is it an expression (has a value)?No, it is a statementYes: val k = x match {...}
Can it look deep inside data?No, only compares one value with constantsYes: case Plus(Const(0), Plus(_, r))
Can it match case classes?NoYes
Can it bind variables?NoYes: case Some(c) => c
Fallthrough and break?Yes, forgetting break runs the next case tooNo fallthrough, no break: exactly one case body runs

Because a match is an expression, it has a type: the lub (least upper bound, see Week 6) of the types of all case bodies.

val n = 7
val kind = n % 2 match { case 0 => "even"; case _ => "odd" }
println(kind)
odd
The rule A match is an expression whose value is the body of the first case that fits. One body runs, never more. Its type is the lub of all case bodies.
Trap Java habits: expecting the next case to run as well (no fallthrough in Scala), or thinking a match without a fitting case just does nothing (it throws MatchError).
On the exam True/false statements like "match is an expression", "match can bind variables", "match has fallthrough" (false). Also type questions where a match has bodies of different types: take the lub.
Try it: value and type
val r = 3 match { case 1 => "one"; case 3 => 3.0; case _ => 'c' }
println(r)
3.0

The second case fits, so the value is 3.0. The static type of r is the lub of String, Double and Char, which is Any (the compiler confirms Any).

Try it: what does this print?
val x = 1
x match { case 1 => println("one"); case 2 => println("two") }
one

Only the first fitting body runs. There is no fallthrough into case 2.

All types of patterns

Here is every kind of pattern you need, with what it fits and what it binds.

PatternExampleFitsBinds
Wildcard_anythingnothing
Constant5, "hello", true, Nil, Nonea value equal (==) to itnothing
Variablex, somethingElse (lowercase)anythingthe whole value
Typeds: String, _: Intvalues whose runtime class is that types, now with static type String
ConstructorPlus(l, Const(0))a Plus whose parts fit the sub-patternswhatever the sub-patterns bind
Tuple(a, b), (Left(x), _)a tuple of that size whose parts fitthe sub-pattern names
ListList(a, b), List(1, _*), h :: t, x :: Nillists of that shapethe names
Binderp @ Plus(Const(0), _)what the right side fitsp = the whole matched value
Guardcase i: Int if i < 0only if the pattern fits and the condition is trueas the pattern

Variable or constant? The capital letter rule

This is the most subtle point. A lowercase name in a pattern is always a new variable that fits anything, even if a val with that name already exists. To compare with an existing value, write it in backticks, or give it an uppercase name.

val limit = 10
def v1(x: Int) = x match {
  case limit => "matched limit, limit = " + limit   // NEW variable, fits everything
}
def v2(x: Int) = x match {
  case `limit` => "ten"                             // compares with the val
  case other   => "other: " + other
}
val Limit = 10
def v3(x: Int) = x match {
  case Limit => "ten"                               // uppercase: a constant
  case _     => "not ten"
}
println(v1(3))
println(v2(3) + " | " + v2(10))
println(v3(3) + " | " + v3(10))
matched limit, limit = 3
other: 3 | ten
not ten | ten

If v1 had a case _ after case limit, the compiler warns: patterns after a variable pattern cannot match (SLS 8.1.1) If you intended to match against value limit ..., you must use backticks. Still only a warning.

Typed patterns, guards, tuples

def describe(x: Any): String = x match {
  case 0               => "zero"
  case i: Int if i < 0 => "negative int " + i
  case i: Int          => "positive int " + i
  case s: String       => "string of length " + s.length
  case (a, b)          => "pair " + a + " and " + b
  case d: Double       => "double " + d
  case _               => "something else"
}
println(List(0, -4, 7, "hey", (1, "x"), 2.5, true).map(describe))
List(zero, negative int -4, positive int 7, string of length 3, pair 1 and x, double 2.5, something else)

A typed pattern is the safe way to "cast": after case s: String you can use s.length, because inside that case s has static type String.

List patterns

def lp(l: List[Int]): String = l match {
  case Nil           => "empty"
  case x :: Nil      => "one element " + x
  case List(a, b)    => "two elements " + a + "," + b
  case 1 :: rest     => "starts with 1, rest " + rest
  case List(_, _, 3) => "three elements ending in 3"
  case h :: t        => "head " + h + " tail " + t
}
println(List(List(), List(5), List(5, 6), List(1, 2, 3, 4), List(7, 8, 3), List(7, 8, 9)).map(lp))
List(empty, one element 5, two elements 5,6, starts with 1, rest List(2, 3, 4), three elements ending in 3, head 7 tail List(8, 9))

x :: Nil and List(x) fit the same lists (exactly one element). List(1, _*) means "starts with 1, then any number of elements".

Patterns are linear, and patterns in val

A name may appear only once in a pattern (patterns are "linear"). To say "both sides are equal", bind two names and use a guard.

case Plus(e, e) => ...                   // not allowed
case Plus(l, r) if l == r => ...         // the correct way
error: e is already defined as value e
  case Plus(e, e) => 2 * e

You can use a pattern after val too. It is handy for tuples. If it does not fit, the program crashes with a MatchError.

val (a, b, _) = ("x", 2, 1.0)
println(a + b)
val List(p, q) = List(1, 2, 3)   // does not fit: three elements
x2
scala.MatchError: List(1, 2, 3) (of class scala.collection.immutable.$colon$colon)
The rule Lowercase name = new variable, fits everything. Backticks or Uppercase = compare with an existing value. _ = fits everything, binds nothing. A guard is checked only after the pattern fits; if the guard is false, matching goes on with the next case.
Trap (1) case limit => looks like a comparison but catches everything. (2) A general case above a specific one (case n: Int above case 5) makes the specific one unreachable. (3) A false guard does not stop the match; the next cases are still tried.
On the exam Expect nested constructor patterns on your own sealed classes (Op(Op(x, op, y), op2, z)), tuple patterns on (this, that), and list patterns h :: t. For each case, line up the pattern against the value part by part, from the outside in.
Try it: binders and guards
def at(e: Exp): String = e match {
  case p @ Plus(Const(0), _)          => "zero on the left in " + p
  case Plus(_, c @ Const(n)) if n > 5 => "big constant " + c + " on the right"
  case Plus(l, r)                     => "plus of " + l + " and " + r
  case other                          => "leaf " + other
}
println(at(Plus(Const(0), Var("x"))))
println(at(Plus(Var("x"), Const(9))))
println(at(Plus(Var("x"), Const(2))))
println(at(Var("y")))
zero on the left in Plus(Const(0),Var(x))
big constant Const(9) on the right
plus of Var(x) and Const(2)
leaf Var(y)

Third call: case 2's pattern fits (n = 2) but the guard 2 > 5 is false, so matching continues with case 3.

Try it: order of typed patterns
def k(x: Any) = x match {
  case n: Int    => "int"
  case 5         => "five"
  case _: String => "str"
  case _         => "?"
}
println(k(5) + " " + k("5") + " " + k(true))
int str ?

5 is an Int, so the first case already fits. case 5 can never be reached by an Int. "5" is a String, not the number 5.

Try it: the lowercase trap

With val limit = 10 as above, what is v1(3)?

matched limit, limit = 3

limit in the pattern is a brand new variable that shadows the outer one and is bound to 3.

Sealed classes

Sealed means: no class outside the current file may extend this class directly. All subclasses are in the same file, so the compiler knows the complete list. That gives you one big benefit: the compiler can check that a match covers every case (it is exhaustive), and warn you if one is missing.

sealed abstract class Shape
case class Circle(r: Double) extends Shape
case class Square(s: Double) extends Shape
case class Tri(a: Double) extends Shape
def area(s: Shape): Double = s match {
  case Circle(r) => 3 * r * r
  case Square(x) => x * x
}
println(area(Square(2)))
println(area(Tri(1)))
warning: match may not be exhaustive.
It would fail on the following input: Tri(_)
4.0
scala.MatchError: Tri(1.0) (of class Main$$anon$1$Tri)

Without sealed, anybody could add a subclass in another file, so the compiler cannot know the list and gives no warning. Then you need a default case (case _) to be safe.

What sealed does not mean:

  • Not final. final means "no subclasses at all"; a sealed class normally has several subclasses, that is the whole point.
  • Not "cannot be abstract". The usual pattern is exactly sealed abstract class (or sealed trait) with case classes under it.
  • Not "methods cannot be overridden". Subclasses in the same file can override methods normally.

About case classes, which you almost always see under a sealed class: their constructor fields are vals (immutable by default), they get == by content, a readable toString, and they can be used as constructor patterns. A case class is not automatically abstract or sealed. A case object is a case class with exactly one value, like None or Nil.

The rule sealed = direct subclasses only in the same file, so the compiler can check exhaustiveness without a default case. A missing case is a compile warning, and a MatchError at runtime if that value really shows up.
Trap Mixing up sealed and final. "A sealed class must be final", "sealed classes cannot be abstract", "no override possible": all false.
On the exam Multiple response statements about sealed and case classes. True: exhaustive matching without a default case; no direct subclasses outside the file; case classes immutable by default; case classes usable in pattern matching.
Try it: compile time or runtime?

In the Shape code above, what does the compiler say, and what happens for area(Square(2)) and area(Tri(1))?

warning: match may not be exhaustive.
It would fail on the following input: Tri(_)
4.0
scala.MatchError: Tri(1.0) (of class Main$$anon$1$Tri)

It compiles (only a warning). Square(2) works. Tri(1) finds no case and throws.

Try it: true or false?

(a) A sealed class can have subclasses. (b) A subclass of a sealed class can override its methods. (c) case class Foo(x: Int) is sealed. (d) The fields of a case class are vals unless you write var.

(a) true, (b) true, (c) false, (d) true.

The problem with null

A reference is the address of an object (an arrow to it). null is the special reference that points to nothing. Its inventor called it "the billion dollar mistake". Two problems:

  • Every reference may be null, so in theory every use needs an extra check. Forget one and you get a NullPointerException at runtime.
  • The type does not tell you. A String may be a real string or null; there is no way in the type to say "this one is never null".
val s: String = null
println(s.length)
java.lang.NullPointerException: Cannot invoke "String.length()" because the return value of "Main$$anon$1.s()" is null

Value types like Int cannot be null at all:

val x: Int = null
error: an expression of type Null is ineligible for implicit conversion

The fix in functional languages: do not use null to mean "no value". Use a type that says it in the open: Option (next topic). Haskell has no null at all (it uses Maybe); Kotlin has String (never null) and String? (may be null); Java 8 added Optional; Scala 2 keeps null for Java compatibility but treats it as a wart.

The rule null compiles wherever a reference type is expected, and fails only at runtime. Option moves the "maybe missing" into the type, so the compiler forces you to handle it.
Trap Thinking the compiler protects you from null. It does not: val s: String = null compiles fine.
On the exam Mostly as a statement ("each reference may be null", "no way to say a reference cannot be null") and as background for Option and for the Null type in Week 6.
Try it: compile error or runtime error?

(a) val s: String = null; s.length (b) val x: Int = null

(a) compiles, then NullPointerException at runtime. (b) compile error: Int is a value type and cannot hold null.

Option

Option[A] is a box that contains either one value or nothing. It is defined with exactly the tools from this week:

sealed abstract class Option[+A]
case class Some[A](a: A) extends Option[A]
case object None extends Option[Nothing]

Some(x) means "there is a value, it is x". None means "there is no value". Because None is an Option[Nothing] and Nothing is a subtype of everything (Week 6), one None works as an Option[String], Option[Int], and so on. A standard use is Map.get:

val capitals = Map("France" -> "Paris", "Japan" -> "Tokyo")
println(capitals.get("France") + " " + capitals.get("North pole"))
println(capitals.getOrElse("North pole", "unknown"))
def show(o: Option[String]): String = o match {
  case Some(c) => "capital " + c
  case None    => "no capital"
}
println(show(capitals.get("Japan")) + " / " + show(capitals.get("Mars")))
Some(Paris) None
unknown
capital Tokyo / no capital

map vs flatMap on an Option

Exam questions often define their own Option (called Maybe) with map and flatMap written as a match. The difference is one wrapper:

sealed abstract class Maybe[+A] {
  def map[B](f: A => B): Maybe[B] = this match {
    case Just(x) => Just(f(x))   // wraps the result of f
    case Empty   => Empty
  }
  def flatMap[B](f: A => Maybe[B]): Maybe[B] = this match {
    case Just(x) => f(x)         // f already returns a Maybe: no extra wrapper
    case Empty   => Empty
  }
}
case class Just[+A](x: A) extends Maybe[A]
case object Empty extends Maybe[Nothing]
def safeDiv(a: Int, b: Int): Maybe[Int] = if (b == 0) Empty else Just(a / b)
println(safeDiv(12, 3).map(safeDiv(_, 2)))
println(safeDiv(12, 3).flatMap(safeDiv(_, 2)))
println(safeDiv(12, 3).flatMap(safeDiv(_, 0)))
println(safeDiv(12, 0).map(_ + 1))
Just(Just(2))
Just(2)
Empty
Empty
The rule map with a function that returns a Maybe gives a Maybe inside a Maybe. flatMap does not add a layer. On Empty/None, both just return Empty/None and never call f.
Trap (1) Flattening when the code uses map, or adding a layer when it uses flatMap. (2) Integer division: 3 / 2 is 1 and 1 / 2 is 0. A result of 0 is still Just(0), not Empty: only dividing by zero gives Empty. (3) Forgetting that Map.get returns an Option, so the printout is Some(...), not the raw value.
On the exam Chains like divOption(3,2).flatMap(divOption(_,2)).zipped(...). Evaluate left to right, write the value after each step (Some(1), then Some(0), ...), and check at every step whether the method adds a wrapper.
Try it: what does this print?
println(safeDiv(7, 2).flatMap(x => safeDiv(x, 4)))
Just(0)

7 / 2 = 3 gives Just(3). Then safeDiv(3, 4) is Just(3 / 4) = Just(0). flatMap adds no layer.

Try it: zip two Maybes
def zip[A, B](a: Maybe[A], b: Maybe[B]): Maybe[(A, B)] = (a, b) match {
  case (Just(x), Just(y)) => Just((x, y))
  case _                  => Empty
}
println(zip(Just(1), Empty) + " " + zip(Just(1), Just("a")))
Empty Just((1,a))

A pair is only produced if both sides are Just. Any Empty falls through to case _.

Try it: map or flatMap?

What do safeDiv(12, 3).map(safeDiv(_, 2)) and safeDiv(12, 3).flatMap(safeDiv(_, 2)) print?

Just(Just(2))
Just(2)

Either

Either[A, B] is like Option, but the "failure" side can carry information. A value is either a Left(a) or a Right(b). By convention Right is the good result and Left is the error (for example an error message). It is an alternative to exceptions.

sealed abstract class Either[A, B]
case class Left[A, B](a: A) extends Either[A, B]
case class Right[A, B](b: B) extends Either[A, B]
def parseAge(s: String): Either[String, Int] =
  if (s.forall(_.isDigit) && s.nonEmpty) Right(s.toInt) else Left("not a number: " + s)
println(parseAge("42") + " " + parseAge("4x2"))
def describeAge(e: Either[String, Int]): String = e match {
  case Right(n) if n >= 18 => "adult"
  case Right(n)            => "minor"
  case Left(msg)           => "error: " + msg
}
println(List("42", "12", "abc").map(s => describeAge(parseAge(s))))
println(parseAge("40").map(_ + 1))
Right(42) Left(not a number: 4x2)
List(adult, minor, error: not a number: abc)
Right(41)

flatMap on an Either continues with a Right and stops at a Left: the Left is passed through unchanged, and the function is never called.

Matching two Eithers at once

A common exam trick is to match on the tuple (this, rhs). Line up each position separately.

def both(a: Either[String, Int], b: Either[String, Int]): String = (a, b) match {
  case (Right(x), Right(y)) => "sum " + (x + y)
  case (Left(m), _)         => "first failed: " + m
  case (_, Left(m))         => "second failed: " + m
}
println(both(Right(1), Right(2)) + " | " + both(Left("a"), Left("b")) + " | " + both(Right(1), Left("b")))
sum 3 | first failed: a | second failed: b
The rule Left = error with information, Right = result. A tuple pattern like (Left(x), Right(y)) only fits if both positions fit. A mismatch goes to the next case, often case _ => None.
Trap (1) Doing the arithmetic anyway when the constructors do not line up: (Left(9.0), Right(2.0)) does not fit (Left(x), Left(y)) nor (Right(x), Right(y)). (2) In a crosswise case like case (Right(y), Left(x)) => f(x, y), the names decide which value goes where, not the position in the tuple.
On the exam "What is Left(9.0).combine(...)(...)(Right(2.0))?" Write the tuple (this, rhs) out in full, then test each case. Also flatMap chains on a custom OrString: once something is a Left, every later flatMap returns that Left.
Try it: flatMap on Either
println(parseAge("20").flatMap(a => if (a > 30) Right(a) else Left("too young")))
println(parseAge("x").flatMap(a => Right(a * 2)))
Left(too young)
Left(not a number: x)

First: Right(20), so the function runs, 20 is not above 30, result Left("too young") (no extra Right around it). Second: it is already a Left, the function is skipped.

Try it: crosswise match
def cross(a: Either[Boolean, Int], b: Either[Boolean, Int]): Option[Int] = (a, b) match {
  case (Left(x), Right(y)) => Some(if (x) y * 2 else y - 1)
  case (Right(y), Left(x)) => Some(if (x) y * 2 else y - 1)
  case _ => None
}
println(cross(Right(5), Left(true)) + " " + cross(Left(false), Right(5)) + " " + cross(Right(5), Right(1)))
Some(10) Some(4) None

First call fits case 2 with y = 5, x = true: 10. Second fits case 1 with x = false, y = 5: 4. Third: two Rights fit neither, so None.

Try it: the pair matcher

What is both(Left("a"), Left("b"))? Why not "second failed"?

first failed: a

Case 2 (Left(m), _) fits first. Order decides.

Tail recursion

Every function call that has not finished yet takes a bit of memory on the call stack (it must remember where to continue). Normal recursion keeps all those unfinished calls around until the deepest one returns. With a million levels, the stack runs out: StackOverflowError.

A call is in tail position if it is the very last thing the function does: its result is returned directly, nothing is left to do afterwards. A function is tail recursive if all its recursive calls are in tail position. Then the compiler can reuse the same stack frame, which turns the recursion into a loop. It is as fast as a while loop and uses stack depth 1.

def sum(n: Int): Int = if (n == 0) 0 else n + sum(n - 1)                  // NOT tail: + waits
def sumAcc(n: Long, acc: Long): Long = if (n == 0) acc else sumAcc(n - 1, acc + n) // tail
println(sum(100))
println(sumAcc(1000000, 0))
println(sum(1000000))
5050
500000500000
Exception in thread "main" java.lang.StackOverflowError
	at Main$$anon$1.sum(...)
	at Main$$anon$1.sum(...)
	...

How to check: ask "what happens after the call returns?"

Look at each recursive call. If the answer is "nothing, its result is returned as is", it is a tail call. If anything still has to happen with the result (n + ..., ... + n, h :: ..., ... ++ List(h), f(...) + f(...), Cons(h, ...)), it is not.

non-tail factorial(4)accumulator facTailRec(4, 1)
4 * factorial(3)facTailRec(3, 4)
4 * (3 * factorial(2))facTailRec(2, 12)
4 * (3 * (2 * factorial(1)))facTailRec(1, 24)
4 * (3 * (2 * (1 * factorial(0))))facTailRec(0, 24)
then multiply back out: 24. Stack depth 524. Stack depth 1

The trick on the right is the accumulating parameter: instead of doing the work after the call, you do it before the call and pass the partial result along. That parameter plays the role of the loop variable in a for loop. (In Haskell, Lisp and ML there are no loops at all; tail recursion is how you loop.)

@tailrec asks the compiler to check

import scala.annotation.tailrec
@tailrec def sum(n: Int): Int = if (n == 0) 0 else n + sum(n - 1)
error: could not optimize @tailrec annotated method sum: it contains a recursive call not in tail position

One more condition: the method may not be overridable. In a class, a public non-final method gives:

error: could not optimize @tailrec annotated method down: it is neither private nor final so can be overridden

Local functions (a def inside a def), methods in an object, and final or private methods are fine.

The rule Tail recursive = every recursive call is the whole result of its branch. A call inside if/else or a match case is fine, as long as nothing is done with its result. Convert with an accumulator: compute first, then call.
Trap (1) "The call is at the end of the line, so it is tail": sum(n-1) + n is written last but the + still waits. (2) fib(n-1) + fib(n-2): two pending calls, not tail. (3) hl :: hr :: vm(tl, tr): the :: waits, not tail. (4) "f calls the nested function g, so f is recursive": f is only recursive if f calls f. If only g calls g, then g is (tail) recursive and f is not recursive at all.
On the exam "Which of these functions are tail recursive?" For each function: underline every recursive call, then ask what happens after it returns. Also statements about a nested g in an f: "f is recursive" (only if f calls itself), "g is tail recursive", "g is a nested function".
Try it: tail recursive or not?
def lastOf(l: List[Int]): Int = l match { case h :: Nil => h; case _ :: t => lastOf(t); case Nil => -1 }
def gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b)
def count(l: List[Int]): Int = l match { case Nil => 0; case _ :: t => 1 + count(t) }
def build(n: Int): List[Int] = if (n == 0) Nil else n :: build(n - 1)

lastOf and gcd: tail recursive (they compile with @tailrec). count (the 1 + waits) and build (the n :: waits): not. With @tailrec both give:

error: could not optimize @tailrec annotated method count: it contains a recursive call not in tail position
error: could not optimize @tailrec annotated method build: it contains a recursive call not in tail position
Try it: trace the accumulator
def sumAcc(n: Int, acc: Int): Int = { println(s"  sumAcc($n, $acc)"); if (n == 0) acc else sumAcc(n - 1, acc + n) }
println(sumAcc(4, 0))
  sumAcc(4, 0)
  sumAcc(3, 4)
  sumAcc(2, 7)
  sumAcc(1, 9)
  sumAcc(0, 10)
10

The work (adding n) happens before each call, so when n reaches 0 the answer is already in acc.

Try it: write it with an accumulator

Write a tail recursive product of a list.

@tailrec def prod(l: List[Int], acc: Int): Int = l match {
  case Nil    => acc
  case h :: t => prod(t, acc * h)
}
println(prod(List(2, 3, 4), 1))
24

Start the accumulator at the neutral element of the operation (1 for *, 0 for +, Nil for building a list).

List: built from :: and Nil

A Scala List is immutable and defined recursively. A list is either

  • the empty list, Nil, or
  • a first element (the head) followed by another list (the tail), written head :: tail (say "cons").

You can write that yourself with a sealed class, exactly as the slides do. This is the shape exam questions use:

sealed abstract class L[+A]
case object Empty extends L[Nothing]
case class Cons[A](head: A, tail: L[A]) extends L[A]

List(1, 2, 3) is really 1 :: (2 :: (3 :: Nil)). Draw it as a tree leaning to the right:

  ::
 /  \
1    ::
    /  \
   2    ::
       /  \
      3    Nil

:: associates to the right, so 1 :: 2 :: 3 :: Nil means 1 :: (2 :: (3 :: Nil)). (Technically an operator ending in : is a method of its right operand: 1 :: l is l.::(1).)

val a = 1 :: 2 :: 3 :: Nil
val b = 1 :: (2 :: (3 :: Nil))
println(a + " " + (a == b) + " " + (a == List(1, 2, 3)))
println(Nil.::(3).::(2))
println(a.head + " " + a.tail + " " + a.tail.tail.tail)
println(0 :: a)
println(a :+ 4)
println(a ++ List(4, 5))
List(1, 2, 3) true true
List(2, 3)
1 List(2, 3) List()
List(0, 1, 2, 3)
List(1, 2, 3, 4)
List(1, 2, 3, 4, 5)

Why lists: simple, immutable, and you process them with pattern matching (case h :: t) and recursion. Downside: adding at the front is cheap, but appending at the back, ++ and indexing walk the whole list (O(n)).

The rule x :: l puts one element in front of a list. l1 ++ l2 glues two lists. :: groups to the right. In a pattern, h :: t splits a non-empty list into its head and tail.
Trap :: with a list on the left does not append, it makes that list one element:
println(List(1, 2) :: List(3, 4))
List(List(1, 2), 3, 4)
On the exam "What does the method gvl/vm do?" on a custom L/Cons/Nil. Run it on a 3-element list by hand, then describe in words: last element, reverse, append, alternate, zip. Also check whether it is tail recursive.
Try it: what does this method do?
def last[A](l: L[A]): Option[A] = l match {
  case Empty          => None
  case Cons(h, Empty) => Some(h)
  case Cons(_, t)     => last(t)
}
val l3 = Cons(1, Cons(2, Cons(3, Empty)))
println(last(l3) + " " + last(Empty))
Some(3) None

It skips heads until the tail is empty: it returns the last element (and it is tail recursive). The order of the cases matters: if Cons(_, t) came before Cons(h, Empty), it would always reach Empty and return None.

Try it: two lists at once
def interleave[A](l: List[A], r: List[A]): List[A] = (l, r) match {
  case (hl :: tl, hr :: tr) => hl :: hr :: interleave(tl, tr)
  case _ => Nil
}
def interleave2[A](l: List[A], r: List[A]): List[A] = (l, r) match {
  case (hl :: tl, hr :: tr) => hl :: hr :: interleave2(tl, tr)
  case (Nil, rest) => rest
  case (rest, Nil) => rest
}
println(interleave(List(1, 2, 3), List(10, 20)))
println(interleave2(List(1, 2, 3), List(10, 20)))
List(1, 10, 2, 20)
List(1, 10, 2, 20, 3)

The first version stops as soon as one list is empty, so the leftover 3 is lost. Neither is tail recursive: hl :: hr :: ... waits for the recursive call.

Try it: zip with a function
def zipWith(l: List[Int], r: List[Int], f: (Int, Int) => Int): List[Int] = (l, r) match {
  case (a :: as, b :: bs) => f(a, b) :: zipWith(as, bs, f)
  case _ => List()
}
println(zipWith(List(5, 6, 7), List(1, 1, 1, 1), _ * _))
List(5, 6, 7)

Three pairs, then the left list is empty and case _ ends it. The fourth 1 is never used. The result has the length of the shorter list.

List: map

map(f) applies f to every element and keeps the order and the length. With pattern matching it is three lines: an empty list stays empty; otherwise transform the head and map the tail.

def map[A, B](l: L[A], f: A => B): L[B] = l match {
  case Empty      => Empty
  case Cons(h, t) => Cons(f(h), map(t, f))
}
println(map(l3, (x: Int) => x * 10))
Cons(10,Cons(20,Cons(30,Empty)))

The same idea works on any recursive structure. On a tree, flatMap replaces every leaf by a whole new tree:

sealed abstract class Tree[A] {
  def flatMap[B](f: A => Tree[B]): Tree[B] = this match {
    case Node(l, r) => Node(l.flatMap(f), r.flatMap(f))
    case Leaf(x)    => f(x)
  }
  def reduce(f: (A, A) => A): A = this match {
    case Node(l, r) => f(l.reduce(f), r.reduce(f))
    case Leaf(x)    => x
  }
}
case class Node[A](l: Tree[A], r: Tree[A]) extends Tree[A]
case class Leaf[A](x: A) extends Tree[A]
The rule map: same shape, every element transformed. Recursion pattern: base case for the empty structure, and "do it to the head, recurse on the rest" (or "recurse on both children").
Trap On a tree, the second flatMap works on every leaf the first one produced, not only on the original one. And map never removes or merges elements, even equal ones.
On the exam Chains like Leaf(2).flatMap(nxt).flatMap(nxt).reduce(_ + _). Draw the tree after each step, then add up all leaves.
Try it: flatMap twice on a tree
def nxt(x: Int): Tree[Int] = Node(Leaf(x), Leaf(x + 10))
val t = Leaf(1).flatMap(nxt).flatMap(nxt)
println(t)
println(t.reduce(_ + _))
Node(Node(Leaf(1),Leaf(11)),Node(Leaf(11),Leaf(21)))
44

Step 1: leaf 1 becomes leaves 1, 11. Step 2: each of those leaves becomes two: 1, 11 and 11, 21. Sum: 1 + 11 + 11 + 21 = 44.

Try it: map that returns lists
println(List(1, 2, 3).map(x => x :: Nil))
List(List(1), List(2), List(3))

map keeps every result as one element, so you get a list of lists. (flatMap would glue them into List(1, 2, 3).)

List: fold

A fold combines all elements into one value, starting from a value z. Written with pattern matching you see exactly where the brackets go:

def foldRight[A, B](l: L[A], z: B, f: (A, B) => B): B = l match {
  case Empty      => z
  case Cons(h, t) => f(h, foldRight(t, z, f))      // not tail recursive
}
@tailrec def foldLeft[A, B](l: L[A], z: B, f: (B, A) => B): B = l match {
  case Empty      => z
  case Cons(h, t) => foldLeft(t, f(z, h), f)       // tail recursive: the accumulator is z
}

For the list 1, 2, 3:

  • foldRight = f(1, f(2, f(3, z))): replace every :: by f and Nil by z. Brackets to the right.
  • foldLeft = f(f(f(z, 1), 2), 3): start with z and eat the elements from the left. Brackets to the left.

foldLeft is a loop with an accumulator (tail recursive). foldRight has to go to the end first and combines on the way back (not tail recursive).

The rule foldRight(z)(f) on 1,2,3 = f(1, f(2, f(3, z))). foldLeft(z)(f) = f(f(f(z, 1), 2), 3). They agree only if f is associative and z is its neutral element (like + with 0).
Trap Swapping the argument order of f: in foldRight the element comes first and the accumulator second; in foldLeft the accumulator comes first.
On the exam Write out the bracket expression first, then compute from the innermost bracket outwards.
Try it: minus both ways
println(foldRight(l3, 0, (x: Int, acc: Int) => x - acc))
println(foldLeft(l3, 0, (acc: Int, x: Int) => acc - x))
2
-6

Right: 1 - (2 - (3 - 0)) = 1 - (2 - 3) = 1 - (-1) = 2. Left: ((0 - 1) - 2) - 3 = -6.

Try it: building a list with a fold
println(foldLeft(l3, Empty: L[Int], (acc: L[Int], x: Int) => Cons(x, acc)))
println(foldRight(l3, Empty: L[Int], (x: Int, acc: L[Int]) => Cons(x, acc)))
Cons(3,Cons(2,Cons(1,Empty)))
Cons(1,Cons(2,Cons(3,Empty)))

foldLeft puts 1 in front first, then 2 in front of that, then 3: reversed. foldRight replaces each Cons by Cons: a copy.

Try it: strings
println(List(1, 2, 3).foldLeft("")((acc, x) => acc + x) + " " + List(1, 2, 3).foldRight("")((x, acc) => acc + x))
123 321

foldRight starts at the end: "" + 3, then + 2, then + 1.

List: reverse

The obvious way: reverse the tail, then put the head at the back.

def append[A](l: L[A], r: L[A]): L[A] = l match {
  case Empty      => r
  case Cons(h, t) => Cons(h, append(t, r))
}
def rev[A](l: L[A]): L[A] = l match {
  case Empty      => Empty
  case Cons(h, t) => append(rev(t), Cons(h, Empty))   // not tail recursive
}
@tailrec def revAcc[A](l: L[A], acc: L[A]): L[A] = l match {
  case Empty      => acc
  case Cons(h, t) => revAcc(t, Cons(h, acc))          // tail recursive
}
println(append(l3, Cons(4, Empty)))
println(rev(l3))
println(revAcc(l3, Empty))
Cons(1,Cons(2,Cons(3,Cons(4,Empty))))
Cons(3,Cons(2,Cons(1,Empty)))
Cons(3,Cons(2,Cons(1,Empty)))

rev is slow (every append walks the list again, so O(n²)) and not tail recursive (the append waits). revAcc moves one element at a time from the front of l to the front of acc, like moving a stack of plates one by one onto a new stack. It is O(n) and tail recursive.

calllacc
1List(1, 2, 3)List()
2List(2, 3)List(1)
3List(3)List(2, 1)
4List()List(3, 2, 1) returned
The rule Reverse with an accumulator: case h :: t => rev(t, h :: acc), start with Nil. The naive version rev(t) ++ List(h) gives the same answer but is not tail recursive.
Trap Thinking a method that rebuilds with the same constructors is a copy. Look at where the head goes: at the back (append(rev(t), Cons(h, Nl))) means reverse; at the front (List(h) ++ r(t)) means copy.
On the exam "What does main print?" with a home-made r/append: trace with a 3-element list. "Which of these is tail recursive?" with rev and revAcc: only the accumulator version.
Try it: reverse or copy?
def r1(l: List[Int]): List[Int] = l match { case h :: t => r1(t) ++ List(h); case Nil => Nil }
def r2(l: List[Int]): List[Int] = l match { case h :: t => List(h) ++ r2(t); case Nil => Nil }
println(r1(List(1, 2, 3)) + " " + r2(List(1, 2, 3)))
List(3, 2, 1) List(1, 2, 3)

Neither is tail recursive: in both, the ++ waits for the recursive call.

Try it: trace revAcc
@tailrec def revAcc(l: List[Int], acc: List[Int]): List[Int] = {
  println(s"  revAcc($l, $acc)")
  l match { case Nil => acc; case h :: t => revAcc(t, h :: acc) }
}
println(revAcc(List(1, 2, 3), Nil))
  revAcc(List(1, 2, 3), List())
  revAcc(List(2, 3), List(1))
  revAcc(List(3), List(2, 1))
  revAcc(List(), List(3, 2, 1))
List(3, 2, 1)

Recap: Week 5 in one screen

ConceptRemember
match orderTop to bottom, first fit wins. Unreachable case = warning. No fit = MatchError at runtime.
simplify questionsCall table. A pattern sees only this node. Only the calls in the body happen. Rebuilt nodes are not re-matched unless res.simplify / fixed point.
match vs switchExpression, deep, case classes, binds variables, no fallthrough.
patterns_, constant, lowercase variable (binds!), `x` or Uppercase constant, x: T, C(p1, p2), (a, b), h :: t, List(1, _*), p @ ..., if guard. Linear: one name once.
sealedSubclasses only in the same file, exhaustiveness check. Not final, can be abstract, override allowed.
nullAny reference may be null; type does not say so; NPE at runtime. Value types cannot be null.
OptionSome(x) / None. map adds a wrapper, flatMap does not. Map.get returns Option.
EitherLeft = error with info, Right = value. flatMap stops at the first Left.
tail recursionNothing left to do after the recursive call. Accumulator. @tailrec checks it (method must be final/private/local).
ListNil or h :: t; :: is right associative. List(1,2) :: List(3) = List(List(1, 2), 3).
map / fold / reversemap keeps shape. foldRight f(1,f(2,f(3,z))), foldLeft f(f(f(z,1),2),3). Reverse with accumulator is tail recursive and O(n).

Week 6: Subtyping

Every expression in Scala has a static type, worked out by the compiler without running anything. This block teaches how the compiler finds that type when values of different classes meet (in a list, in an if, in a tuple, in a var): the least upper bound, or lub. The exam asks "what is the most specific type of x", "which types does x have" and "what can we fill in for T", and you answer all of them with one method: draw the hierarchy, walk up, find the first common ancestor.

What does "A is a subtype of B" mean?

We write A <: B and say "A is a subtype of B". The professor gives three ways to say the same thing:

  • A supports at least the interface of B (every method you can call on a B, you can call on an A);
  • so an A can be used anywhere a B is expected (this is called substitution);
  • example: Dog <: Animal. Wherever the code wants an Animal, you may give it a Dog.

Every type is a subtype of itself (A <: A). And it is transitive: if Dog <: Mammal and Mammal <: Animal, then Dog <: Animal.

Subtype is not the same as subclass. A subclass (class Dog extends Animal) is a subtype, but there are other subtypes too: a class is a subtype of every trait it mixes in, Null is a subtype of every class type, Nothing of every type, and function and generic types have their own subtype rules (variance).

Static type versus runtime class

Two different things are going on, and keeping them apart solves many questions:

  • The static type is what the compiler knows about an expression, from declarations. It decides what you are allowed to write.
  • The runtime class is the class the object was really created with (new Dog). It decides which method body runs when a method is overridden (dynamic dispatch).
abstract class Animal { def speak: String }
class Dog extends Animal { def speak = bark; def bark = "woof" }
val someAnimal: Animal = new Dog
println(someAnimal.bark)
error: value bark is not a member of Animal

The object is a Dog, but the variable has static type Animal, and Animal has no bark. The professor's phrase: "types are static; making a type more generic makes the type system forget". But calling an overridden method uses the real object:

abstract class Vehicle { def name: String = "vehicle" }
abstract class Car extends Vehicle { override def name = "car" }
class Sedan extends Car { override def name = "sedan" }
class Hatchback extends Car
abstract class Bike extends Vehicle
class RoadBike extends Bike { override def name = "roadbike" }
val vs: List[Vehicle] = List(new Sedan, new RoadBike, new Hatchback)
println(vs.map(_.name))
List(sedan, roadbike, car)

Hatchback does not override name, so it uses the nearest one above it, from Car.

The rule A <: B: an A can be used wherever a B is expected. The compiler checks calls against the static type; at runtime, the object's own class picks the method body.
Trap (1) "It really is a Dog, so bark works": no, compile error. (2) The opposite: "the list is a List[A], so A.a runs for every element": no, each object runs its own override.
On the exam "The above program will: give a type error / print ...". First check every call against the static type. Only if everything compiles, run it using the runtime classes.
Try it: which method runs?
class A { def a = "a" }
class B extends A { override def a = "b" }
def f(l: List[A]) = l.map(_.a)
println(f(List(new B, new A, new B)))
List(b, a, b)

The parameter type List[A] only matters for type checking. Each element runs its own a.

Try it: compile or not?

With the Dog code above, does someAnimal.speak compile, and what is it? And someAnimal.bark?

speak is declared in Animal, so it compiles, and at runtime Dog's version runs: println(someAnimal.speak) prints woof. bark is not declared in Animal: error: value bark is not a member of Animal.

The Scala type hierarchy

All Scala types sit in one big tree. Learn this picture; you will draw it on scrap paper in the exam.

Any AnyValAnyRef DoubleIntCharBooleanUnit StringList[A]your classes Null Nothing conversion, not subtype
  • Any: the top. Every type is a subtype of Any. Methods like ==, toString, isInstanceOf live here.
  • AnyVal: the value types: Int, Double, Long, Char, Boolean, Unit, ... They cannot be null.
  • AnyRef: the reference types: String, List, every class and trait you write. They can be null. (AnyRef is Java's Object; the compiler sometimes prints it as Object.)
  • Null: the type of null, a subtype of every AnyRef type (not of AnyVal types).
  • Nothing: at the very bottom, a subtype of every type, with no values at all.

Important: Int is not a subtype of Double. They are siblings under AnyVal. Scala can convert an Int into a Double automatically (numeric widening, the dashed line), and it does so when Int and Double meet in a list or an if. That is a conversion of the value, not subtyping.

println(List(3, 2.0))
println(List(3, 'c'))
println(List(3, true))
List(3.0, 2.0)
List(3, 99)
List(3, true)

The 3 became 3.0 and the 'c' became its character code 99: those values were converted. The third list is not converted, because Int and Boolean have no conversion between them. The compiler gives these types:

expressiontype (checked)
List(3, 2.0)List[Double]
List(3, 'c')List[Int]
List(3, true)List[AnyVal]
List(new Sedan, "text")List[Object], that is List[AnyRef]
List(new Sedan, 3)List[Any]
The rule Any on top; AnyVal (Int, Double, Boolean, Char, Unit) and AnyRef (String, List, your classes) below it; Null under all AnyRef types; Nothing under everything. A value type and a reference type only meet at Any.
Trap lub(Int, String) is Any, not AnyRef or AnyVal. lub(Int, Boolean) is AnyVal, not Any. lub(String, your class) is AnyRef. And numbers are the one exception where values get converted: List(3, 2.0) is List[Double].
On the exam Any question with a list or if that mixes numbers, strings, booleans and objects. Put each type in the picture, walk up from each, and stop at the first box they share.
Try it: what is the most specific type?

(a) List(3, true) (b) List(new Sedan, "text") (c) List(new Sedan, 3)

(a) List[AnyVal] (b) List[AnyRef] (printed List[Object]) (c) List[Any]. All three checked with the compiler.

Try it: what does this print?
println(List(3, 'c'))
List(3, 99)

Char and Int meet as Int (a numeric conversion), so 'c' is turned into its code 99.

Values vs references

In Scala everything is an object, but there are two kinds of types:

  • Value types (subtypes of AnyVal: Int, Double, Boolean, ...). Think of the variable as holding the number itself. They cannot be null.
  • Reference types (subtypes of AnyRef: String, arrays, every class you define). The variable holds a reference: an arrow to an object somewhere in memory. They can be null.

The consequence: when you copy a reference (val b = a, arr(i) = c, passing it to a function), you copy the arrow, not the object. Both arrows point to the same object. If that object is mutable, a change through one arrow is visible through the other. Paper method: draw a box for each object, an arrow for each variable or array slot, and redraw arrows on every assignment.

class Cell(var v: Int)              // mutable
val cc = new Cell(1)
val arr = Array(cc, cc, new Cell(1))  // slots 0 and 1 point to the SAME cell
arr(0).v += 5                         // changes that one shared cell
println(arr.map(_.v).toList)

case class ICell(v: Int)            // immutable
val d = ICell(1)
val arr2 = Array(d, d, ICell(1))
arr2(0) = ICell(arr2(0).v + 5)      // slot 0 gets a NEW object; slot 1 still points to d
println(arr2.map(_.v).toList)
List(6, 6, 1)
List(6, 1, 1)

Same structure, one change (mutate the shared object vs put a new object in one slot), different answer. Value types never have this problem:

var i = 3; var j = i; j += 1
println(i + " " + j)
3 4

An immutable collection (immutable Map, List, Vector) never changes. Its "change" methods (updated, +, union) return a new collection, so in an immutable API they return the collection type, never Unit. But "immutable" is about the collection only: if it holds references to mutable objects, those objects can still change.

The rule Assigning a reference copies the arrow, not the object. Mutating an object is seen through every arrow to it; reassigning one variable or slot only moves that one arrow.
Trap (1) val means the arrow cannot be moved, not that the object cannot change. (2) m.updated(...) on an immutable map does nothing to m unless you store the result. (3) A method add(x): Unit on a "immutable" Set is a contradiction: returning nothing means it must have changed something.
On the exam Arrays filled with the same object, then one slot mutated or replaced; maps updated and read through another name. Draw boxes and arrows and keep them up to date line by line. (The Week 1/2 lessons on aliasing go deeper.)
Try it: shared or not?

Predict both lines of the Cell/ICell code above before opening.

List(6, 6, 1)
List(6, 1, 1)

First: slots 0 and 1 are arrows to one mutable cell, so both show 6. Second: slot 0 got a new ICell(6); slot 1 still points to the old ICell(1).

Try it: null by kind

Which compile? var w: Bird = null, var s: String = null, var i: Int = null.

The first two (reference types). The third fails: error: an expression of type Null is ineligible for implicit conversion.

Many types per value

A value does not have one type: it has its most specific type and every supertype of that. The number 8 has type Int (most specific), but also AnyVal and Any:

val x8: AnyVal = 8
val y8: Any = 8
println(x8 + " " + y8)
8 8

For a list, List is covariant (a List[Dog] is also a List[Animal]; the variance lesson explains why). So a list has the types List[S] for every supertype S of its element type, plus Any (and AnyRef) for the list object itself.

Method for "which types does x have?"

  1. Find the most specific type of x (with the lub, next topics).
  2. List every ancestor of it: superclasses, traits mixed in anywhere above it, AnyRef, Any.
  3. Wrap each ancestor in the same container (List[...], tuple components separately).
  4. Add plain Any. Never Nothing: no value has type Nothing.
  5. Reject any option that only fits some of the elements.
trait Electric
class Tesla extends Car with Electric
class EBike extends Bike with Electric
val l1: List[Electric] = List(new Tesla, new EBike)   // OK
val l2: List[Vehicle]  = List(new Tesla, new EBike)   // OK
val l3: List[AnyRef]   = List(new Tesla, new EBike)   // OK
val l4: Any            = List(new Tesla, new EBike)   // OK
val l5: List[Car]      = List(new Tesla, new EBike)   // error
error: type mismatch;
 found   : EBike
 required: Car

The static type comes from declarations, not from the object

This decides many "most specific type" questions. The type of a method call is the method's declared return type, whatever object goes in:

def same[A](x: A): A = x                  // generic: returns exactly the type that went in
def up(x: Any): Any = x                   // declared Any: the compiler forgets everything
def bounded[A <: Vehicle](x: A): A = x    // bound only restricts A; result is still A
def toCar(x: Any): Car = new Sedan        // result type Car, whatever the argument
case class Holder(v: Vehicle)             // field declared Vehicle
case class GHolder[A](v: A)               // generic field
expressionstatic type (checked)
List(same(new Sedan), bounded(new Hatchback))List[Car]
List(up(new Sedan), new Sedan)List[Any]
List(toCar(new RoadBike), new Sedan)List[Car]
List(bounded(new Sedan), bounded(new Tesla))List[Car]
(Holder(new Sedan).v, GHolder(new Sedan).v)(Vehicle, Sedan)

Note also def up2(x: Any) = x without a declared result: the inferred result type is the type of x, which is Any. A generic [A] is what keeps the type.

The rule A value has its most specific type and all supertypes of it. The static type of a call is the declared result type (with type parameters filled in from the arguments). A bound A <: X is a restriction, not the result type.
Trap (1) Picking a trait or class that only some elements have (List[Electric] works for Tesla+EBike, but not if a Sedan joins). (2) Using the argument's class for a method that returns Any or a fixed class. (3) Thinking a bound A <: Fruit makes the result Fruit. (4) val l = List(false, "hallo", 3); val x: Boolean = l(0) is a type error, because l(0) has static type Any, whatever is stored there:
error: type mismatch;
 found   : Any
 required: Boolean
On the exam "What types does l have? Select 0 or more" with a class diagram including traits. Draw the diagram, mark which ancestors every element has (intersection), then tick List[X] for each shared X, List[Any] and Any.
Try it: which types?

For List(new Tesla, new EBike): which of List[Electric], List[Vehicle], List[Car], List[Any], Any, Nothing?

List[Electric], List[Vehicle], List[Any], Any. Not List[Car] (EBike is not a Car, compile error above), not Nothing.

Try it: boxes

What is the type of (Holder(new Sedan).v, GHolder(new Sedan).v)?

(Vehicle, Sedan)

Holder's field is declared Vehicle, so the compiler forgets it was a Sedan. GHolder is generic, so A = Sedan and the field type is Sedan.

Try it: function results

Most specific type of List(toCar(new RoadBike), new Sedan) and of List(up(new Sedan), new Sedan)?

List[Car] (toCar is declared to return Car; lub(Car, Sedan) = Car) and List[Any] (up returns Any). Both checked with the compiler.

Null

Null is a type with exactly one value: null. It is a subtype of every reference type (every AnyRef type), so you can always use null where any class type is expected. "Null supports any interface", which is exactly why it is dangerous: the compiler lets you call any method on it, and it fails at runtime.

class Animal
class Bird extends Animal
var w: Bird = null       // fine: Null <: Bird
var i: Int = 4
i = null                 // Int is a value type
error: an expression of type Null is ineligible for implicit conversion
i = null

For the lub this means: null disappears into any reference type, but next to a value type you go up to Any.

expressiontype (checked)
List("a", null)List[String]
if (c) "a" else nullString
List(3, null)List[Any]
The rule Null <: every AnyRef type, not AnyVal types. lub(String, Null) = String; lub(Int, Null) = Any.
Trap Thinking null belongs to every type. val x: Int = null does not compile.
On the exam Options like releaseFun(new SetFree[...](null)) use null just as a dummy argument: it fits any reference type, so only the type arguments matter. And lub questions with a null branch.
Try it: types with null

Types of if (c) "a" else null and List(3, null)?

String and List[Any].

Try it: compile?

val nv: Vehicle = null; println(nv)

null

Compiles and prints null. println(nv.name) compiles too, and then fails at runtime:

java.lang.NullPointerException: Cannot invoke "Main$$anon$1$Vehicle.name()" because the return value of "Main$$anon$1.nv()" is null

Nothing

Nothing is at the bottom: a subtype of every type. It has no values. So what has type Nothing? An expression that never produces a value: throw new Exception(...), or a call to a function that always throws.

def fail(msg: String): Nothing = throw new Exception(msg)
def divide(x: Int, y: Int): Int =
  if (y == 0) fail("Division by zero")   // Nothing
  else x / y                              // Int

The if has type lub(Nothing, Int) = Int, so divide type checks. Nothing makes the type rules consistent: an error branch never spoils the type of the other branch.

expressiontype (checked)
throw new Exception("x")Nothing
if (c) 10 else fail("no")Int
List(new Sedan, throw new Exception("x"))List[Sedan]
List(), NoneList[Nothing], None.type (an Option[Nothing])

Because Nothing <: X for every X, the empty list List() fits List[Sedan], None fits any Option[T], and in every "which of these are subtypes of X?" question, the option Nothing is correct (even for an invariant type like Array[Mammal]).

The other side: a parameter of type Nothing can only receive something that never produces a value.

def g(a: Int, b: Nothing): Int = a
g(1, 3)
error: type mismatch;
 found   : Int(3)
 required: Nothing
g(1, throw new Exception("boom"))   // compiles; the throw runs when the argument is evaluated
java.lang.Exception: boom
The rule Nothing <: everything, and it has no values. throw has type Nothing. In a lub, Nothing disappears: lub(X, Nothing) = X.
Trap (1) Mixing up Nothing (bottom, below everything) with Any (top). "Which types does this value have?": never Nothing. "Which types are subtypes of X?": always Nothing. (2) A list containing a throwing call is not List[Nothing]: Nothing vanishes in the lub. (3) Do not "run" the code to find a type: List(new Dog, fid(new Dog)) with fid returning Nothing has type List[Dog], even though it would throw.
On the exam Lists and ifs with a throw/error(...) element: drop it from the lub. Variance multiple choice: tick Nothing. "Which calls do not give a type error?" with a Nothing parameter: only the one passing a throw.
Try it: types with throw

Most specific types of if (c) 10 else fail("no") and List(new Sedan, throw new Exception("x"))?

Int and List[Sedan].

Try it: which call compiles?

With def g(a: Int, b: Nothing): Int = a: (a) g(1, 3) (b) g(1, null) (c) g(1, throw new Exception("boom"))

Only (c). Not even null fits: Null is a subtype of reference types, but Nothing is below Null. The compiler says for (b):

error: type mismatch;
 found   : Null(null)
 required: Nothing

(c) compiles, and at runtime the throw happens while the argument is evaluated: java.lang.Exception: boom.

Lub: the least upper bound

When values of different types end up together (a list, the two branches of an if, ...), the compiler needs one type that fits all of them. Any common supertype is an upper bound. The least upper bound, lub, is the most specific one. In the class tree it is the lowest common ancestor (counting a type as its own ancestor). In biology: the most recent common ancestor.

The professor's rule for lists: List(a: A, b: B, c: C) has type List[lub(A, B, C)].

The method: draw, walk up, first common ancestor

AnyAnyRef Vehicletrait Electric CarBike SedanHatchbackTesla RoadBikeEBike
trait Electric
abstract class Vehicle
abstract class Car extends Vehicle
class Sedan extends Car
class Hatchback extends Car
class Tesla extends Car with Electric
abstract class Bike extends Vehicle
class RoadBike extends Bike
class EBike extends Bike with Electric
  1. Draw the hierarchy (classes solid, traits dashed). Put AnyRef and Any on top.
  2. For the first type, walk up and mark every ancestor (all paths, including traits).
  3. Do the same for the second type. For more types, repeat: lub(A, B, C) = lub(lub(A, B), C).
  4. The lub is the first common ancestor: the lowest box marked by all of them.
typeswalklub (checked)
Sedan, Hatchbackboth reach Car firstCar
Sedan, RoadBikeCar / Bike differ, meet at VehicleVehicle
Tesla, SedanTesla's Electric is not shared; Car isCar
Tesla, EBikeshared: Vehicle and Electric, neither above the otherElectric with Vehicle
Tesla, EBike, SedanSedan has no ElectricVehicle
Sedan, Nothing (a throw)Nothing is below everythingSedan

The Tesla/EBike row is the one case where there is no single lowest box: the two lowest shared ancestors are unrelated. Then the compiler's most specific type is "both at once", Electric with Vehicle, and List[Electric] and List[Vehicle] are both valid types. Exam options usually ask about the single types.

A side note: for case classes the compiler often prints extra parts, like Product with Maybe[Int] with java.io.Serializable for if (c) Just(3) else Empty. Those come from case classes automatically extending Product and Serializable. In exam answers, ignore them and take the class from the diagram (Maybe[Int]).

lub of function types

A list of functions also gets a lub. A function type A => B is a subtype of C => D if it accepts at least what the other accepts and returns at most what the other returns (arguments go the opposite way, the variance lesson explains this). So for the lub of several function types:

  • argument: the most specific of the argument types, the one that is a subtype of all of them (walk down). Only arguments every function accepts are safe.
  • result: the lub of the result types (walk up), as always.
val f1: Car => Sedan = x => new Sedan
val f2: Sedan => Car = x => x
val f3: Vehicle => Hatchback = x => new Hatchback
List(f1, f2, f3)    // List[Sedan => Car]
List(f1, f3)        // List[Car => Car]

Arguments Car, Sedan, Vehicle: the lowest one is Sedan. Results Sedan, Car, Hatchback: lub is Car. (Both types checked with the compiler.)

The rule lub = lowest common ancestor. Draw, walk up from every type, take the first box all of them reach. Nothing and Null (for class types) vanish. For function types: arguments go down, results go up.
Trap (1) Climbing too far (answering Animal or Any when Mammal already covers all). (2) Stopping too low (a trait or class that only some elements have). (3) Forgetting traits mixed in higher up: if C extends H, then every subclass of C is also an H. (4) For function lists, taking the widest argument type (like Any): wrong direction.
On the exam "What is the most specific type of l? (Tip: draw a diagram)". Always draw it, even for five classes. Then fill a small table: element, its static type (declared return type if it is a call), its ancestors. Intersect. One quirk of past answers: when class types with no shared class or trait meet (say a Raccoon and an ApatoSaurus), the compiler says AnyRef (printed Object), but the answer options usually only offer Any. If AnyRef is not an option, Any is the intended answer.
Try it: two lists

Most specific types of List(new Tesla, new Sedan, new Hatchback) and List(new Tesla, new EBike, new Sedan)?

List[Car] and List[Vehicle] (compiler confirms both). In the second, Electric is not shared by Sedan, and Car is not shared by EBike.

Try it: list of functions

With f1: Car => Sedan and f3: Vehicle => Hatchback, what is the type of List(f1, f3)?

List[Car => Car]. Arguments Car and Vehicle: the lower one, Car (both functions accept a Car). Results: lub(Sedan, Hatchback) = Car.

Try it: generic calls

Type of List(same(new Sedan), bounded(new Hatchback))?

List[Car]. same returns a Sedan, bounded returns a Hatchback (the bound <: Vehicle does not widen it), lub = Car.

The if rule: lub of the branches

An if/else is an expression. At compile time the compiler does not know which branch will run, so the type must fit both:

(if (c) x else y) : lub(type of x, type of y)

Examples from the slides: if (c) x: Int else y: String has type Any; if (c) cat else koala has type Mammal. The same holds for a match (lub of all case bodies) and for the branches of a function body.

If without else: Scala adds else (), and () has type Unit, a value type. So the type is lub(T, Unit).

expressiontype (checked)
if (c) new Sedan else new TeslaCar
if (c) new Sedan else 7Any
if (c) 5AnyVal (lub of Int and Unit)
if (c) "x"Any (lub of String and Unit)
if (c) 10 else fail("no")Int
3 match { case 1 => "one"; case 2 => 2; case _ => fail("x") }Any
3 match { case 1 => new Sedan; case _ => new Hatchback }Car
The rule type of if/else = lub of both branches, even when the condition is a constant. Missing else = Unit branch. A branch that throws = Nothing, and drops out.
Trap (1) Using only the branch that will actually run: the type ignores the condition's value. (2) Forgetting the hidden Unit of an if without else.
On the exam def x(c: Boolean) = if (c) ... else ...: "most specific return type". Compute the lub of the branches; if the branches are tuples, go to the next topic.
Try it: without else

Types of if (c) 5 and if (c) "x"?

AnyVal and Any. Unit is a value type, so it meets Int at AnyVal, but String only at Any.

Try it: numbers in branches
val c = true
val r = if (c) 1 else 2.0
println(r)
1.0

The type of r is Double (Int and Double meet as Double through numeric widening), so the 1 is converted and prints as 1.0.

Try it: a match

Type of 3 match { case 1 => new Sedan; case _ => new Hatchback }?

Car, the lub of all case bodies.

Lub and tuples

A tuple type (A, B) is a subtype of (C, D) if A <: C and B <: D. So the lub of tuples is taken component by component:

lub((A, B), (C, D)) = (lub(A, C), lub(B, D))

Slide example: lub((String, Koala), (Int, Cat)) = (lub(String, Int), lub(Koala, Cat)) = (Any, Mammal).

Method: write the two branches under each other, draw a vertical line between the components, and do a separate lub per column. Inside a column, apply all the other rules (lists, Option, Nothing, ...).

expressiontype (checked)
if (c) (new Sedan, 1) else (new RoadBike, "x")(Vehicle, Any)
if (c) (List(new Sedan), new Tesla) else (List(new Hatchback, new Tesla), new EBike)(List[Car], Electric with Vehicle)
if (c) (List(new Sedan), Some(new Tesla)) else (List(new RoadBike), None)(List[Vehicle], Option[Tesla])

The last row: column 2 is lub(Some[Tesla], None). None is an Option[Nothing], so they meet at Option[Tesla] (not Some[Tesla]: None is not a Some).

The rule Tuples: one lub per position, independently. Each column can end up at a different level.
Trap (1) Taking one lub for all components together. (2) Mixing up which components belong together (first with first). (3) Keeping Some[X] when the other branch has None: it must become Option[X]/Maybe[X].
On the exam "Which return types does v have? Select 0 or more" with tuples of lists. Compute the most specific tuple, then accept every option that is a supertype per component (plus Any), and reject options with an Either or a component that misses one branch.
Try it: column by column

Type of if (c) (new Sedan, 1) else (new RoadBike, "x")?

(Vehicle, Any): lub(Sedan, RoadBike) = Vehicle; lub(Int, String) = Any.

Try it: with a list and a trait

Type of if (c) (List(new Sedan), new Tesla) else (List(new Hatchback, new Tesla), new EBike)?

(List[Car], Electric with Vehicle). Column 1: all list elements are Cars. Column 2: Tesla and EBike share Vehicle and Electric.

Try it: is this annotation allowed?
val t1: (List[Vehicle], Option[Car]) =
  if (c) (List(new Sedan), Some(new Tesla)) else (List(new RoadBike), None)

Yes, it compiles: the most specific type is (List[Vehicle], Option[Tesla]), and Option[Tesla] is an Option[Car] because Option is covariant. (List[Car], ...) would fail because of the RoadBike.

Lub and variables

A variable gets its type once, when it is declared: either the type you write, or (if you write none) the type of the initial value. Later assignments do not change it; each one must fit that type.

class Mammal
class Koala extends Mammal
class Cat extends Mammal
var x = new Koala     // inferred type: Koala
x = new Cat
error: type mismatch;
 found   : Cat
 required: Koala
var x: Mammal = new Koala   // declared wider
x = new Cat                 // fine

The slide rule: the type of a variable must be (at least) the lub of all values it will ever hold. Since the compiler only looks at the initializer, you must declare that wider type. The same goes for tuples: var t = (new Koala, "a"); t = (new Cat, "b") fails with found: Cat, required: Koala.

And a val cannot be assigned again at all:

val m = (1, "a")
m = ("b", 2)
error: reassignment to val

"What can we fill in for T?": two checks

  1. Every value assigned to the variable (initializer and all later assignments) must be a subtype of T. So T must be at or above the lub of all of them.
  2. Every member used on the variable later must exist on T. So T must not be too high.
trait P { def x: String = "p" }
trait Q { def y: String = "q" }
class A extends P
class C extends P
class D extends C with Q
def run(b: Boolean): String = {
  var p: T = (new A, new D)
  if (b) p = (new C, p._2)
  p._1.x + p._2.y
}
Tcheck 1: (A, D) and (C, D) fit?check 2: _1.x and _2.y exist?compiler
(P, Q)yesyesruns, prints pq pq
(P, D)yesyesruns, prints pq pq
(A, D)no: C is not an Afound: C, required: A
(P, C)yesno: C has no yvalue y is not a member of C
(Any, Any)yesnovalue x is not a member of Any
The rule A var keeps its declared or inferred type forever. Fill-in-T: lower limit = lub of all assigned values; upper limit = the members you call. Anything in between works. A val can never be reassigned: "always type error".
Trap (1) Picking the most specific type of the first assignment only. (2) Picking Any or (Any, Any) because "it fits everything": it fails check 2 if a method is called. (3) Missing a val that is reassigned: then no T works.
On the exam var v: ?? = new B; v = new C; v = new D: write the ancestor set of each class and intersect. With a list (var p: ??? = List(new B, new C)): T must be List[X] with X an ancestor of every element of every list assigned.
Try it: what can we fill in?
trait Red
trait Round
trait Small extends Red with Round
class Ball extends Round
class Apple extends Small
class Cherry extends Ball with Red
class Tomato extends Ball with Small
var v: ?? = new Apple
v = new Cherry
v = new Tomato

Options: Red, Round, Small, Ball, AnyRef.

Ancestors: Apple: Small, Red, Round. Cherry: Ball, Round, Red. Tomato: Ball, Round, Small, Red. Shared: Red, Round, and AnyRef/Any. The compiler agrees: Red, Round and AnyRef compile; Small fails at v = new Cherry (found: Cherry, required: Small); Ball fails at the first line (found: Apple, required: Ball).

Try it: inferred type of a var
var v = List(new Sedan)
v = List(new Hatchback)
error: type mismatch;
 found   : Hatchback
 required: Sedan

v was inferred as List[Sedan]. Declaring var v: List[Car] = ... fixes it.

Try it: val swap

val m: T = (new Sedan, "42"); m = (m._2, m._1). Which T works?

None: error: reassignment to val, whatever T is.

Casting and conversion

Every value has two methods from Any to look at and change its type at runtime:

  • obj.isInstanceOf[String]: is this object (at runtime) a String? Gives a Boolean.
  • obj.asInstanceOf[String]: treat it as a String. If it really is one, you get it with static type String; otherwise ClassCastException.

The professor calls this the "back door of the type system": you tell the compiler "shut up, I know what I'm doing". And: casting is bad, avoid it. If you know it is a String and the compiler does not, redesign. A typed pattern does the same check and cast in one safe step.

def describe(s: Any): String =
  if (s.isInstanceOf[String]) "a string of length " + s.asInstanceOf[String].length
  else "not a string: " + s
def describe2(s: Any): String = s match {     // the better way
  case str: String => "a string of length " + str.length
  case other       => "not a string: " + other
}
println(describe("hallo") + " | " + describe(9))
println(describe2("hallo") + " | " + describe2(9))
val boxed: Any = 9
println(boxed.asInstanceOf[String])
a string of length 5 | not a string: 9
a string of length 5 | not a string: 9
java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (...)

Conversion is different, and fine

A conversion makes a new value of another type: 3.toDouble, 3.7.toInt, "42".toInt. The slide: casts that can fail are bad, but converting between numbers is OK. Some numeric conversions happen automatically (Int to Double, called widening); the narrowing direction never does.

println(3.toDouble + " " + 3.7.toInt + " " + (-3.7).toInt + " " + 'a'.toInt + " " + 97.toChar + " " + "42".toInt + " " + 7 / 2 + " " + 7 / 2.0)
val d: Double = 3
println(d)
val i: Int = 3.0
3.0 3 -3 97 a 42 3 3.5
3.0
error: type mismatch;
 found   : Double(3.0)
 required: Int
The rule Cast (asInstanceOf) = same object, new static type, may fail at runtime. Conversion (toInt, toDouble) = a new value. Prefer a typed pattern over isInstanceOf plus asInstanceOf.
Trap (1) A cast does not change the object: casting an Animal that is not a Dog to Dog compiles, then throws ClassCastException. (2) toInt cuts off, it does not round: 3.7.toInt is 3, (-3.7).toInt is -3. (3) 7 / 2 is 3 (Int division).
On the exam Statements like "asInstanceOf throws an exception if the object is not of that type", "casting is the back door of the type system", "isInstanceOf checks the runtime type". And conversion results inside code-output questions.
Try it: compile, run, or crash?
class Animal; class Dog extends Animal { def bark = "woof" }
val a: Animal = new Dog
println(a.isInstanceOf[Dog] + " " + a.asInstanceOf[Dog].bark)
val a2: Animal = new Animal
println(a2.asInstanceOf[Dog].bark)
true woof
java.lang.ClassCastException: class Main$$anon$1$Animal cannot be cast to class Main$$anon$1$Dog (...)

Both lines compile (a cast silences the compiler). The first object is a Dog, so it works. The second is not, so it crashes at runtime.

Try it: conversions

What do 3.7.toInt, (-3.7).toInt, 'a'.toInt, 7 / 2 and 7 / 2.0 give?

3 -3 97 3 3.5

Recap: Week 6 subtyping in one screen

ConceptRemember
A <: BA supports at least B's interface; an A can be used wherever a B is expected. Subtype is not only subclass (traits, Null, Nothing, functions).
static vs runtimeCompiler checks calls against the static type (someAnimal.bark fails). Overridden methods run the object's own version.
hierarchyAny > AnyVal (Int, Double, Boolean, Char, Unit) and AnyRef (String, List, classes). Null under AnyRef types. Nothing under all.
values vs referencesAnyVal: no null. AnyRef: variable holds an arrow; copying copies the arrow.
many typesMost specific type plus every supertype; List[S] for each ancestor S; plus Any; never Nothing. Calls have their declared result type.
Null / Nothinglub(String, Null) = String, lub(Int, Null) = Any. throw : Nothing; lub(X, Nothing) = X; Nothing is a subtype of every type.
lubDraw, walk up from each type, first common ancestor. Two unrelated shared ancestors: X with T. Functions: arguments down, results up.
key lubs(Int, Double) = Double (conversion), (Int, Boolean) = AnyVal, (Int, String) = Any, (String, class) = AnyRef, (Some[X], None) = Option[X], (T, Unit) for if without else.
if / matchlub of all branches; condition value is ignored.
tuplesComponentwise lub.
variablesType fixed at declaration. Fill-in T: above the lub of all assigned values, low enough for every member used. val reassignment = always error.
cast vs conversionasInstanceOf may throw ClassCastException (back door, avoid; use a typed pattern). toInt/toDouble make new values; toInt truncates.

Week 6 (part 2): Traits, variance and type bounds

This block is about two ways Scala lets one type stand in for another. Traits let a class have many "parents", which raises the question of which method wins (the diamond problem, linearization, stacked super calls). Variance decides when Box[Dog] may be used as a Box[Animal], including for function types. The exam asks these as "what does this print", "which types does this list have" and "which of these are subtypes", and small changes (the order of with, a + or -, mutable or immutable) flip the answer.

Traits: one superclass, many traits

First some words you need.

  • A class is a blueprint for objects. class Dog extends Animal makes Dog a subclass of Animal, and Animal the superclass of Dog. Dog inherits every method of Animal.
  • A concrete method has a body (def speak = "Woof"). An abstract method has only a signature (def speak: String), and some subclass must give the body.
  • To override a method is to give a new body for a method you inherited. Scala wants the keyword override when you replace a concrete method.
  • A <: B means "A is a subtype of B": a value of type A can be used everywhere a B is expected (the professor: "D supports at least the interface of A").

A trait is like an abstract class: it can have abstract methods and concrete methods. The differences that the exam tests:

  • A class can extend only one class, but it can mix in many traits: class Duck extends Animal with Swimmer with Flyer. The first parent comes after extends, every next one after with.
  • A trait has no constructor (no parameters) in Scala 2.
  • A trait defines a type. A Duck is a Duck, an Animal, a Swimmer, a Flyer, an AnyRef and an Any, all at once.
  • A trait can extend another trait, and can contain concrete methods (it is not limited to abstract ones).
trait Swimmer { def swim: String = "paddle"; def name: String }
trait Flyer { def fly: String = "flap"; def name: String }
class Animal { def sound: String = "..." }
class Duck extends Animal with Swimmer with Flyer {
  def name = "duck"
  override def sound = "quack"
}
val d = new Duck
println(d.name + " " + d.swim + " " + d.fly + " " + d.sound)
val s: Swimmer = d   // a Duck is a Swimmer
val f: Flyer = d     // ... and a Flyer
val a: Animal = d    // ... and an Animal
println(s.swim + " " + f.fly + " " + a.sound)
duck paddle flap quack
paddle flap quack

The same object d was stored in three variables with three different static types. Each variable only lets you call what its type promises: s.fly would not compile, because a Swimmer does not promise fly. The object itself never changes.

What Scala refuses, with the real errors:

class Animal
class Plant
class Weird extends Animal with Plant   // two classes
trait Named(val name: String)           // trait with a constructor
error: class Plant needs to be a trait to be mixed in
error: traits or objects may not have parameters

Who wins: the class itself beats its traits

When you call a method on an object, Scala searches for the body starting at the object's own class. If the class defines the method itself, that body wins, no matter what the traits say. Only when the class does not define it, the search moves into the traits (the exact order is the next two topics).

trait X { def x = "x1" }
trait Y extends X { override def x = "x2"; def y = "y1" }
trait Z { def z = "z1" }
class K extends Z with Y { override def z = "z2" }
val k = new K
println(k.x + k.y + k.z)
x2y1z2

Trace: x is defined in X and overridden in Y; K does not define it, so Y's version wins: "x2". y only exists in Y: "y1". z is defined in Z, but K overrides it: "z2".

All the types of a value: a graph, not a tree

With only classes, the type hierarchy is a tree (one parent each). With traits, a class has several parents, so the hierarchy becomes a directed acyclic graph (DAG). The professor: "Lub generalizes to DAG: it gives a set of types rather than one". The lub (least upper bound) of some types is their most specific common supertype.

Exam questions show a few traits and classes and ask "what types does l have" for val l = List(new G, new H, new I). Paper method:

  1. For each element, write the set of all its ancestors: its class, everything after extends and with, and then (recursively) their parents. Do not forget parents of parents.
  2. Intersect the sets. A type is a common supertype only if every element has it. "Two out of three" does not count.
  3. The list has type List[T] for every T in the intersection, plus List[Any], plus Any itself (List is covariant, see below, so List[T] <: List[Any] <: Any).
  4. The list never has type Nothing, Null or the class of just one element.
trait Red; trait Round; trait Sweet; trait Food
class Fruit extends Food
class Apple extends Fruit with Red with Round with Sweet
class Cherry extends Fruit with Red with Sweet
class Tomato extends Red with Round
class Ball extends Round
val l = List(new Apple, new Cherry, new Tomato)
val ok1: List[Red] = l
val ok2: List[AnyRef] = l
val ok3: List[Any] = l
val ok4: Any = l
var v: Round = new Apple   // one type that fits all three assignments
v = new Tomato
v = new Ball
println("all compile")
all compile
ElementAll ancestors (besides AnyRef, Any)
AppleApple, Fruit, Food, Red, Round, Sweet
CherryCherry, Fruit, Food, Red, Sweet
TomatoTomato, Red, Round
IntersectionRed

So l has the types List[Red], List[AnyRef], List[Any] and Any. Not List[Sweet] (Tomato is not Sweet) and not List[Fruit] (Tomato is not a Fruit). The compiler agrees:

trait Red; trait Round; trait Sweet; trait Food
class Fruit extends Food
class Apple extends Fruit with Red with Round with Sweet
class Cherry extends Fruit with Red with Sweet
class Tomato extends Red with Round
val l = List(new Apple, new Cherry, new Tomato)
val bad1: List[Sweet] = l
val bad2: List[Fruit] = l
val l2 = List(new Apple, new Cherry)
val show: Int = l2   // deliberately wrong, to make the compiler print the inferred type
error: type mismatch;
 found   : List[Red]
 required: List[Sweet]
val bad1: List[Sweet] = l
                        ^
error: type mismatch;
 found   : List[Red]
 required: List[Fruit]
val bad2: List[Fruit] = l
                        ^
error: type mismatch;
 found   : List[Fruit with Red with Sweet]
 required: Int
val show: Int = l2   // deliberately wrong, to make the compiler print the inferred type

The last error shows something new: when several traits are shared, Scala writes the lub as one combined type with with, here Fruit with Red with Sweet. For the exam you do not have to write those; you only check, option by option, whether every element has that type.

The same method answers "what can we fill in for ??" in var v: ?? = new A; v = new B; v = new C, and in def f(i: Int): ?? = if (...) new A else if (...) new B else new C. The type of a variable must fit every value ever assigned to it (the "lub and variables" rule), and the type of an if is the lub of its branches.

The rule A class extends one class and mixes in any number of traits. A trait is an abstract-class-like type with no constructor; it may contain concrete methods. A value has every type of every ancestor; a common type of several values must be an ancestor of each one of them.
Trap Picking a trait because "most" elements have it. In List(new Apple, new Cherry, new Tomato), Sweet is shared by two of three, so List[Sweet] is wrong. Another trap: forgetting inherited traits. If class C extends H and class E extends C with I, then an E is also an H, even though H is not written on E's line.
Trap "A trait can only have abstract methods" is false (Winged's travelsInHours on the slides has a body). "A class can extend multiple traits" and "a class can only have one superclass" are both true. sealed and final are different things: a sealed class can still extend traits; a final method cannot be overridden (error: cannot override final member).
On the exam "What types does l have? Tip: draw a diagram." Draw every trait and class as a box with arrows to its parents. For each element, colour everything reachable from it. Tick an option List[T] only if T is coloured for all elements. Always tick List[Any] and Any if offered; never tick Nothing.
Try it: what does this print?
trait Engine { def start = "vroom"; def wheels = 4 }
trait Electric extends Engine { override def start = "hum" }
trait Toy { def wheels = 3 }
class Car extends Toy with Electric { override def wheels = 2 }
val c = new Car
println(c.start + c.wheels)

Answer: hum2.

Linearization search for start: Car does not define it, Electric (last trait) does: "hum". For wheels: Car defines it itself, so 2, and the bodies in Toy and Engine are never used.

hum2

Small change: delete override def wheels = 2 from Car. Now Toy and Engine both give an unrelated concrete wheels, and Scala refuses:

error: class Car inherits conflicting members:
  def wheels: Int (defined in trait Toy) and
  def wheels: Int (defined in trait Engine)
  (note: this can be resolved by declaring an `override` in class Car.)
Try it: which types does l3 have?

Same traits and classes as above. val l3 = List(new Apple, new Tomato, new Ball). Options: List[Red], List[Round], List[Fruit], List[Any], Any, Nothing.

Answer: List[Round], List[Any], Any.

Ancestors: Apple {Fruit, Food, Red, Round, Sweet}, Tomato {Red, Round}, Ball {Round}. Only Round is in all three. Red fails on Ball, Fruit fails on Tomato and Ball. Nothing is below everything, never above. The compiler's inferred type:

error: type mismatch;
 found   : List[Round]
 required: Int
Try it: which types can replace ???
var v: ?? = new Cherry
v = new Apple
v = new Tomato

Options: Red, Sweet, Fruit, Round, Any.

Answer: Red and Any.

The variable's type must be an ancestor of Cherry, Apple and Tomato. Sweet and Fruit fail on Tomato; Round fails on Cherry (it fails on the very first line). Real errors for the three wrong ones:

error: type mismatch;
 found   : Tomato
 required: Sweet
var v2: Sweet = new Cherry; v2 = new Apple; v2 = new Tomato
                                                 ^
error: type mismatch;
 found   : Tomato
 required: Fruit
var v3: Fruit = new Cherry; v3 = new Apple; v3 = new Tomato
                                                 ^
error: type mismatch;
 found   : Cherry
 required: Round
var v4: Round = new Cherry; v4 = new Apple; v4 = new Tomato
                ^

The diamond problem

Multiple inheritance (several parents) brings one classic question. Draw it:

A: xB: xC: xD

A has a method x. B and C both extend A and both override x. D extends B and C but does not override x. Which x does D have? The shape (A on top, D at the bottom) is a diamond, hence the name.

Scala's answer, from the slides: trait D extends C with B gets x from B; trait D extends B with C gets x from C. The trait mentioned last wins.

trait P { def a: String = "P"; def both: String = a + a }
trait Q extends P { override def a = "Q" }
trait R extends P { override def a = "R" }
class QR extends Q with R
class RQ extends R with Q
println((new QR).both + " " + (new RQ).both)
class QRown extends Q with R { override def a = "own" }
println((new QRown).both)
RR QQ
ownown

Trace for (new QR).both:

  1. both is only defined in P, so P's body runs: a + a.
  2. Inside that body, a means this.a, and this is a QR object. The call is dynamically dispatched: Scala looks up a starting from the real object, not from P. In Q with R, R is last, so a is "R".
  3. Result "RR". For RQ the last trait is Q: "QQ". If the class overrides a itself, the class wins: "ownown".

This is the rule from the previous topic again, in one sentence: "start at the class, then the last trait, then the one before it".

When there is no common parent

The rule above works because Q and R both override the same method from P. If two traits that are not related both define a concrete method with the same name, Scala does not guess:

trait B { def a: String = "B" }
trait C { def a: String = "C" }
class D extends B with C
error: class D inherits conflicting members:
  def a: String (defined in trait B) and
  def a: String (defined in trait C)
  (note: this can be resolved by declaring an `override` in class D.)

The fix the compiler suggests: override it in the class. Inside, super[B].a picks one parent explicitly:

trait B { def a: String = "B" }
trait C { def a: String = "C" }
class D extends B with C { override def a = "D:" + super[B].a + super[C].a }
println((new D).a)
D:BC

In other languages (slide)

LanguageMultiple parentsDiamond
Scalaone class, many traitsresolved by linearization (last trait wins)
Javaone class, many interfaces (Scala's traits are called interfaces there)compiler error
C++no interfaces/traits, multiple inheritance of classescompiler error
Haskellno subtyping; type classes are "somewhat similar, but not the same"does not arise
The rule If the class does not define the method, the version from the trait mentioned last in extends ... with ... with ... wins. A method body in a parent that calls another method still calls the version the real object has (dynamic dispatch). Two unrelated concrete methods with the same name: compile error unless the class overrides.
Trap Answering "BB" for class D extends B with C because B is mentioned first. First mentioned is searched last. Answering "AA" because the body of b lives in A: the body is A's, but the a it calls is the object's. Expecting a compile error: with a common parent and override, Scala compiles it fine.
On the exam "What is the value of d?" with a diamond: (1) find the class of the object (after new); (2) for every method call, start the search at that class, then the traits from right to left; (3) when a found body calls another method, start the search again from the class, not from where the body was.
Try it: what does this print?
trait Base { def id = "base"; def show = "[" + id + "]" }
trait L extends Base { override def id = "L" }
trait Rt extends Base { override def id = "R" }
class LR extends L with Rt
class RL extends Rt with L
class LR2 extends L with Rt { override def show = "<" + id + ">" }
println((new LR).show + (new RL).show + (new LR2).show)

Answer: [R][L]<R>.

LR: show only exists in Base, which calls id; the last trait in L with Rt is Rt, so [R]. RL: last is L, so [L]. LR2 overrides show itself, so its own body runs, and inside it id still comes from Rt: <R>.

[R][L]<R>
Try it: which line changes the answer?

Take class QR extends Q with R from the example (prints RR). Which of these changes make it print QQ? (a) swap to extends R with Q; (b) move def both from P into Q; (c) add override def a = "Q" to QR.

Answer: (a) and (c). (a) makes Q the last trait (we ran RQ: QQ). (c) makes the class win, and its own a is "Q". (b) does not change which a the object has: both still calls this.a, which is still R's, so it still prints RR (we ran that variant too: RR).

Linearization: deepest first, last trait first

Scala solves every diamond the same way: it puts the class and all its ancestors in one row, the linearization. A method is searched in that row from left to right, and the first body found is used. The professor's slogan for the order: deepest first, last trait first. "Deepest" means the most specific: the class itself comes before its parents, and a trait comes before the traits it extends.

The paper method

  1. Write the row of the first parent (the one after extends), ending with AnyRef, Any. A parent's own row is computed by this same method.
  2. Go through the other parents from left to right. For each one, write its own row, cross out every name that is already in your row, and put what is left in front of your row.
  3. Finally put the class itself in front.

The slide example:

trait Animal { def who: String = "Animal" }
trait Furry extends Animal { override def who = "Furry>" + super.who }
trait HasLegs extends Animal { override def who = "HasLegs>" + super.who }
trait FourLegged extends HasLegs { override def who = "FourLegged>" + super.who }
class Cat extends Animal with Furry with FourLegged { override def who = "Cat>" + super.who }
println((new Cat).who)
StepParent addedIts own rowNew part (not yet present)Row so far
1AnimalAnimal, AnyRef, AnyallAnimal, AnyRef, Any
2FurryFurry, Animal, AnyRef, AnyFurryFurry, Animal, AnyRef, Any
3FourLeggedFourLegged, HasLegs, Animal, AnyRef, AnyFourLegged, HasLegsFourLegged, HasLegs, Furry, Animal, AnyRef, Any
4(the class)CatCat, FourLegged, HasLegs, Furry, Animal, AnyRef, Any

That is exactly the red "search order" arrow on the slide. Each who prints its name and then calls super.who, so the program prints the row:

Cat>FourLegged>HasLegs>Furry>Animal

Check the slogan: the class first (deepest). Then the last trait, FourLegged, and directly after it HasLegs, because HasLegs is "behind" FourLegged and is not shared with anyone else. Animal, which everybody shares, is pushed to the very end, just before AnyRef and Any.

Another way to see it. A shared ancestor always ends up as late as possible: after every trait that extends it. That is why A in a diamond is searched last, and why "the trait mentioned first" (Animal or B) is searched after the later ones.

super in a trait means "next in the row"

In a class, super.m means "the superclass's m". In a trait it is different: super.m means the next m to the right in the linearization of the actual object. That is decided only when the trait is mixed into a class, so the same trait can call different things in different classes.

trait A { def m: String = "A" }
trait B extends A { override def m = "B" + super.m }
trait C extends A { override def m = "C" + super.m }
class D1 extends B with C
class D2 extends C with B
class D3 extends B with C { override def m = "D" + super.m }
println((new D1).m + " " + (new D2).m + " " + (new D3).m)
CBA BCA DCBA
ClassLinearizationm unfolds asResult
D1 extends B with CD1, C, B, A"C" + (B's m) = "C" + ("B" + (A's m))CBA
D2 extends C with BD2, B, C, A"B" + ("C" + "A")BCA
D3 extends B with C, own mD3, C, B, A"D" + ("C" + ("B" + "A"))DCBA

The surprise: in D1, super.m inside C calls B, although C does not extend B. C was written with A as its parent, but in D1's row the next one after C is B.

Stacked traits with printing

When traits wrap each other with super, think of a call stack: each trait does some work, calls the next one in the row, waits for the answer, and then finishes its own work.

class Greeter {
  def greet(): String = { println("  Greeter.greet"); "hi" }
}
trait Polite extends Greeter {
  override def greet(): String = {
    println("  enter Polite"); val r = "please, " + super.greet(); println("  leave Polite"); r
  }
}
trait Loud extends Greeter {
  override def greet(): String = {
    println("  enter Loud"); val r = super.greet().toUpperCase + "!"; println("  leave Loud"); r
  }
}
class G1 extends Greeter with Polite with Loud
class G2 extends Greeter with Loud with Polite
println("G1:"); println((new G1).greet())
println("G2:"); println((new G2).greet())
G1:
  enter Loud
  enter Polite
  Greeter.greet
  leave Polite
  leave Loud
PLEASE, HI!
G2:
  enter Polite
  enter Loud
  Greeter.greet
  leave Loud
  leave Polite
please, HI!

Trace for G1 (row: G1, Loud, Polite, Greeter):

CallPrintsWaits forReturns
Loud.greetenter Loudsuper = Polite.greet"PLEASE, HI" + "!" = "PLEASE, HI!" (prints leave Loud)
Polite.greetenter Politesuper = Greeter.greet"please, " + "hi" (prints leave Polite)
Greeter.greetGreeter.greetnothing"hi"

The "enter" lines come in row order (Loud first, the last trait), the "leave" lines in reverse. The last trait is the outermost wrapper, so its work happens last on the result: Loud upper-cases everything, including Polite's "please". Swap the traits (G2) and Polite becomes outermost: "please, " + "HI!".

The rule Row = the class, then (built from the right) each trait's own chain, with shared ancestors kept only at their last position, ending in AnyRef, Any. A method call takes the first body in the row. super.m inside a trait goes to the next body to the right in that object's row.
Trap Building the row naively right to left and keeping the first copy of a shared trait: for Cat that would put Animal right after HasLegs, before Furry, which is wrong. Shared ancestors go to the back. Also: you cannot mix in the same trait twice (error: trait Incr is inherited twice).
On the exam For any output question with traits and super: (1) write the linearization row of the object's class; (2) start at the left with the method called; (3) every super.m moves one step right to the next body of m; (4) build the result inside out, like a stack. Write the row down; doing it in your head is where errors happen.
Try it: linearize K and M
trait T { def who: String = "T" }
trait U extends T { override def who = "U>" + super.who }
trait V extends T { override def who = "V>" + super.who }
trait W extends V { override def who = "W>" + super.who }
class K extends U with W { override def who = "K>" + super.who }
class M extends W with U { override def who = "M>" + super.who }
println((new K).who)
println((new M).who)

Answer: K>W>V>U>T and M>U>W>V>T.

K extends U with W. Step 1: row of U is U, T. Step 2: W's row is W, V, T; T is present, new part W, V goes in front: W, V, U, T. Step 3: K in front.

M extends W with U. Step 1: W, V, T. Step 2: U's row U, T, new part U: U, W, V, T. Then M.

K>W>V>U>T
M>U>W>V>T
Try it: a harder one
trait A { def who: String = "A" }
trait B extends A { override def who = "B>" + super.who }
trait C extends B { override def who = "C>" + super.who }
trait D extends A { override def who = "D>" + super.who }
class E extends C with D with B { override def who = "E>" + super.who }
class F extends D with C
println((new E).who)
println((new F).who)

Answer: E>D>C>B>A and C>B>D>A.

E extends C with D with B. Start with C's row: C, B, A. Add D (row D, A): new part D, so D, C, B, A. Add B (row B, A): nothing new, B stays where it is. Then E in front. Note that mentioning B last does not move B to the front, because it was already in the row as part of C.

F extends D with C (and has no own who). Start D, A. Add C (row C, B, A): new part C, B, so C, B, D, A. F has no body, so the first body is C's.

E>D>C>B>A
C>B>D>A
Try it: stacked arithmetic
class Counter { def add(x: Int): Int = { println(s"  Counter.add($x)"); x } }
trait Doubling extends Counter { override def add(x: Int) = { println(s"  Doubling.add($x)"); super.add(x * 2) } }
trait Incr extends Counter { override def add(x: Int) = { println(s"  Incr.add($x)"); super.add(x + 1) } }
class C1 extends Counter with Doubling with Incr
class C2 extends Counter with Incr with Doubling
class C3 extends Counter with Incr with Doubling {
  override def add(x: Int) = { println(s"  C3.add($x)"); super.add(x) - 100 }
}
println("C1:"); println((new C1).add(5))
println("C2:"); println((new C2).add(5))
println("C3:"); println((new C3).add(5))

Answer: 12, 11 and -89, with these prints:

C1:
  Incr.add(5)
  Doubling.add(6)
  Counter.add(12)
12
C2:
  Doubling.add(5)
  Incr.add(10)
  Counter.add(11)
11
C3:
  C3.add(5)
  Doubling.add(5)
  Incr.add(10)
  Counter.add(11)
-89

C1 row: C1, Incr, Doubling, Counter. Incr adds 1 (6), passes to Doubling which doubles (12), Counter returns 12. C2 row: C2, Doubling, Incr, Counter: double first (10), then +1 (11). C3 has its own add, which runs first, passes 5 on unchanged, gets 11 back and subtracts 100: -89.

Variance: the big idea, and one method for every question

A generic (parametric) class has a type parameter: class Box[A]. A is the type parameter; in Box[Dog], Dog is the type argument. Now the question: we know Dog <: Animal. Is Box[Dog] <: Box[Animal]? There are three possible answers, and the class declares which one by a sign in front of A:

DeclarationNameRuleExampleIntuition (slide)
Box[+A]covariantBox[X] <: Box[Y] iff X <: Y (same direction)List, Option, tuples, function resultsreading
Box[-A]contravariantBox[X] <: Box[Y] iff Y <: X (direction flips)Printer, function argumentswriting
Box[A]invariantBox[X] <: Box[Y] iff X = YArray, mutable cells, any class without a signreading and writing

Function types are not special syntax for variance: A => B is the class Function1[-A, +B]. So the whole topic is one method applied again and again.

The method: "is S a subtype of T?"

  1. Free answers first. Nothing is a subtype of every type, so it is always an answer when offered. Any is a subtype of nothing except Any. Null is a subtype of every reference type.
  2. Same outer constructor? Compare the outside first. Some[X] <: Option[X] because Some extends Option; List[...] is never a subtype of Array[...]. If the outer classes do not match by inheritance, the answer is no.
  3. Look up the sign of each type parameter (in the declaration, not in the option): + means the inside must go the same direction, - means it must go the opposite direction, no sign means the inside must be exactly equal.
  4. Recurse. The inside is again a type, so apply steps 1 to 3 to it, with the direction you got. A - inside a - flips twice, so it points the same way again.
  5. Functions: A => B <: C => D iff C <: A (argument: flipped) and B <: D (result: same).

Write the needed relation down for each part, like this for "is Printer[Box[Animal]] a subtype of Printer[Box[Bird]]?" (with Printer[-A], Box[+A]):

LevelNeedWhy
Printer[...] vs Printer[...]Box[Bird] <: Box[Animal]Printer is -: flip
Box[...] vs Box[...]Bird <: AnimalBox is +: keep
classestrueBird extends Animal

So yes. The next topics explain why each sign is what it is, which is what lets you answer when the exam changes one detail.

The rule + keeps the direction, - flips it, no sign demands equality. Functions are Function1[-A, +B]. Nothing is below everything.
Trap Using the class hierarchy straight through every type constructor ("Dog is an Animal, so anything of Dog is anything of Animal"). That is only true for +. And look at the declaration given in the question: exam questions often redefine class List[+A], class Printer[-A] or class MutVar[A] at the top.
On the exam Every "which are subtypes of ..." and "which calls do not give a type error" question is this method. Write the target type at the top of your scrap paper, mark each layer with its sign (+, -, =), and check each option layer by layer. Mark Nothing as correct straight away.
Try it: which class definitions compile?
class R1[+A](val x: A) { def get: A = x }
class R2[+A] { def set(a: A): Unit = () }
class W1[-A] { def put(a: A): Unit = () }
class W2[-A](val x: A)
class C1[A](var x: A)

Answer: R1, W1 and C1 compile; R2 and W2 do not.

R1: a +A only comes out (val, result): fine. R2: a +A goes in as an argument: forbidden. W1: a -A goes in: fine. W2: a val of type A lets a value come out, forbidden for -A. C1: invariant A may appear anywhere, even in a var.

error: covariant type A occurs in contravariant position in type A of value a
class R2[+A] { def set(a: A): Unit = () }
                       ^
error: contravariant type A occurs in covariant position in type A of value x
class W2[-A](val x: A)
                 ^
Try it: apply the method to a function type

Box is +, Printer is -. Which are subtypes of Printer[Bird] => Box[Bird]? (a) Printer[Parrot] => Box[Parrot] (b) Printer[Animal] => Box[Bird] (c) Printer[Nothing] => Box[Nothing]

Answer: a and c. Argument (flip): need Printer[Bird] <: arg; Printer flips again, so Printer[X] with X Bird or below: Parrot and Nothing pass, Animal fails. Result (keep): Box[Parrot] and Box[Nothing] are below Box[Bird]. Compiler-checked.

Covariance: List[+A]

The puzzle from the slides: a function wants a List[Animal], and we pass a List[Dog]. Why does that work?

abstract class Animal { def speak: String }
class Dog extends Animal { def speak = "Woof" }
class Cat extends Animal { def speak = "Miauw" }

def printAll(l: List[Animal]): Unit = l.foreach(x => println(x.speak))
val dogs: List[Dog] = List(new Dog, new Dog)
printAll(dogs)                         // List[Dog] is accepted as a List[Animal]

val withCat = new Cat :: dogs          // a NEW list; dogs is unchanged
println(withCat.map(_.speak))
println(dogs.map(_.speak))
Woof
Woof
List(Miauw, Woof, Woof)
List(Woof, Woof)

Because List is declared List[+A]: covariant. List[Dog] <: List[Animal] because Dog <: Animal. This is safe because printAll can only read from the list, and everything it reads is a Dog, which is an Animal.

"But I can add a Cat to it!" No: a List is immutable. new Cat :: dogs makes a new list, and its type is the lub of Cat and Dog, so List[Animal]. The old dogs is untouched and is still all dogs. You cannot pretend the new list is a list of dogs:

error: type mismatch;
 found   : List[Animal]
 required: List[Dog]

The same happens with dogs.updated(0, new Cat) in the slides: the result is a List[Animal], so dogs = setCat(dogs) does not compile. Immutability is what makes covariance safe.

Covariance and Nothing

Since Nothing <: X for every X, covariance gives List[Nothing] <: List[X] for every X. That is why one empty list object works for every list type: the slide declares case object Empty extends List[Nothing]. Same for None extends Option[Nothing]: None is a subtype of Option[Option[Whatever]], and Some[None.type] is a subtype of Option[Option[X]].

What a covariant class may not do

If Box[+A] had a method that takes an A as argument, covariance would break: treat a Box[Dog] as a Box[Animal] and pass a Cat to that method. So the compiler forbids A in argument positions (and a var, which has a hidden setter x_= that takes an A):

class Box[+A](val x: A) { def put(y: A): Box[A] = new Box(y) }
class Sink[-A] { def get: A = ??? }
class Cell[+A](var x: A)
error: covariant type A occurs in contravariant position in type A of value y
class Box[+A](val x: A) { def put(y: A): Box[A] = new Box(y) }
                                  ^
error: contravariant type A occurs in covariant position in type A of method get
class Sink[-A] { def get: A = ??? }
                     ^
error: covariant type A occurs in contravariant position in type A of value x_=
class Cell[+A](var x: A)
                   ^

"Contravariant position" is the compiler's word for "a place where values go in" (method arguments). "Covariant position" means "where values come out" (results, val fields). A +A may only appear where values come out, a -A only where values go in. The fix for adding an element is a lower bound [B >: A], explained in the type bounds topic.

The rule F[+A]: F[X] <: F[Y] iff X <: Y. Safe only when F never takes an A in (immutable, read-only). Built-in covariant types you meet: List, Option/Some, Seq, tuples (each component), function results.
Trap Thinking List[Any] is a subtype of List[Bird] because Any is "big". Covariance keeps direction, so List[Any] is a supertype of List[Bird]. In an option list, Read[Any] is not a subtype of Read[Write[Int]] for the same reason.
On the exam "Which are subtypes of Box[X]" with Box[+A]: tick Nothing, Box[Nothing], and every Box[Y] where Y is X or below X. If Y is itself a generic type, recurse with the method.
Try it: subtypes of Box[Bird]
class Animal; class Bird extends Animal; class Parrot extends Bird
class Box[+A]

Which are subtypes of Box[Bird]: Box[Parrot], Box[Animal], Box[Nothing], Nothing?

Answer: Box[Parrot], Box[Nothing], Nothing. Box[Animal] is a supertype (Animal is above Bird). Checked with the compiler (implicitly[Box[Animal] <:< Box[Bird]] fails with Cannot prove that Box[Animal] <:< Box[Bird]).

Try it: covariance inside covariance

With Box[+A] and Printer[-A]: is Box[Printer[Animal]] a subtype of Box[Printer[Bird]]? And Box[Printer[Parrot]]?

Answer: yes and no. Box is +: need Printer[Animal] <: Printer[Bird]. Printer is -: need Bird <: Animal: true. For Parrot we would need Bird <: Parrot: false. (Both checked with the compiler.)

Invariance: why a mutable Array cannot be covariant

Arrays can be changed in place: a(0) = x. Suppose arrays were covariant, so an Array[Dog] could be passed as an Array[Animal]. Then this would compile:

abstract class Animal { def speak: String }
class Dog extends Animal { def speak = "Woof" }
class Cat extends Animal { def speak = "Miauw" }
def setCat(a: Array[Animal]): Unit = a(0) = new Cat
val dogs = Array(new Dog, new Dog)
setCat(dogs)

Inside setCat, putting a Cat into an Array[Animal] is perfectly fine. But the array is really the dogs array. After the call, dogs(0) would be a Cat, while its type says Dog: calling dogs(0).bark would crash. Scala prevents this: Array is invariant, and the call does not compile.

error: type mismatch;
 found   : Array[Dog]
 required: Array[Animal]
Note: Dog <: Animal, but class Array is invariant in type T.

Java (and C#) made arrays covariant. The slide calls that a mistake: Java compiles the same program and only fails at runtime.

public class Cats {
  static void setCat(Animal[] a) { a[0] = new Cat(); }
  public static void main(String[] args) {
    Dog[] dogs = new Dog[3];
    setCat(dogs);          // compiles: Java arrays are covariant
    System.out.println("never printed");
  }
}

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
Exception in thread "main" java.lang.ArrayStoreException: Cat

Why not contravariant either? Then an Array[Animal] could be used as an Array[Dog], and reading a(0) would give you "a Dog" that might be a Cat. A mutable container is both read and written, so neither direction is safe: only equality. Slide summary: covariance = reading, contravariance = writing, invariance = reading and writing.

The same holds for any class you write without a sign, for example class MutVar[A](var x: A):

error: type mismatch;
 found   : MutVar[Dog]
 required: MutVar[Animal]
Note: Dog <: Animal, but class MutVar is invariant in type A.

Invariance only blocks the inside. Outside, other rules still apply: an invariant thing can sit inside a covariant one. List[Some[MutVar[Animal]]] <: List[Maybe[MutVar[Animal]]] holds (List is +, Some extends Maybe, and the MutVar parts are equal), but List[Maybe[MutVar[Dog]]] is not a subtype (the MutVar parts differ).

The rule No sign: F[X] <: F[Y] only if X and Y are the same type. Arrays and mutable classes are invariant because values go both in and out. The only subtypes of Array[Mammal] are Array[Mammal] itself, Nothing (and Null).
Trap Applying function variance or class inheritance through an invariant class. If SetFree[A] is invariant, SetFree[Animal => Capybara] is not accepted where SetFree[Mammal => Mammal] is wanted, even though Animal => Capybara <: Mammal => Mammal. Same with Array[C => A]: the only answers are the identical type and Nothing.
Trap An expression of static type Any is never accepted where a MutVar[D] is wanted, even if its value happens to be null: the type checker looks at the type (null : Any), not the value. A throw expression has type Nothing and is always accepted.
On the exam Before anything else, find out which classes are invariant (no sign, or Array). For every invariant layer, the inner types in the option must be written exactly the same as in the target.
Try it: subtypes of Cell[Bird]

class Cell[A]. Which are subtypes of Cell[Bird]: Cell[Parrot], Cell[Animal], Cell[Nothing], Nothing?

Answer: only Nothing (and Cell[Bird] itself, not offered). Cell[Nothing] is a trap: Nothing is below Bird, but invariance demands equality. All four checked with the compiler.

Try it: mixing layers

Box is +, Cell invariant, Printer -. Which are true?

  1. Box[Cell[Parrot]] <: Box[Cell[Bird]]
  2. (Parrot, Printer[Animal]) <: (Bird, Printer[Bird])
  3. (Parrot, Cell[Parrot]) <: (Bird, Cell[Bird])
  4. (Nothing, Nothing) <: (Bird, Cell[Bird])

Answer: b and d. (a) Box is fine but the Cell layer needs equality. (b) Tuples are covariant per component: Parrot below Bird, and Printer flips so Animal above Bird is right. (c) the second component fails on Cell. (d) Nothing fits every component. Compiler-checked.

Contravariance: Printer[-A]

Some things only take values in. A printer that can print any Animal can certainly print a Cat. So wherever a "Cat printer" is needed, an "Animal printer" will do: Printer[Animal] <: Printer[Cat]. The direction is flipped compared to Animal and Cat. That is contravariance, declared with -.

abstract class Animal { def speak: String }
class Dog extends Animal { def speak = "Woof" }
class Cat extends Animal { def speak = "Miauw" }
abstract class Printer[-A] { def print(value: A): Unit }
class AnimalPrinter extends Printer[Animal] {
  def print(a: Animal) = println("The animal says " + a.speak)
}
class CatPrinter extends Printer[Cat] {
  def print(c: Cat) = println("The cat says " + c.speak)
}
def printMyCat(p: Printer[Cat], c: Cat): Unit = p.print(c)
printMyCat(new CatPrinter, new Cat)
printMyCat(new AnimalPrinter, new Cat)   // Printer[Animal] <: Printer[Cat]
The cat says Miauw
The animal says Miauw

The other way round is not allowed. A CatPrinter only knows how to print cats; if we could pass it where a Printer[Animal] is needed, someone might give it a Dog:

abstract class Animal { def speak: String }
class Dog extends Animal { def speak = "Woof" }
class Cat extends Animal { def speak = "Miauw" }
abstract class Printer[-A] { def print(value: A): Unit }
class CatPrinter extends Printer[Cat] {
  def print(c: Cat) = println("The cat says " + c.speak)
}
def printAnyAnimal(p: Printer[Animal], a: Animal): Unit = p.print(a)
printAnyAnimal(new CatPrinter, new Dog)
error: type mismatch;
 found   : CatPrinter
 required: Printer[Animal]

Mirror image of covariance: a -A may appear where values go in (arguments) but not in results; class Sink[-A] { def get: A } gives contravariant type A occurs in covariant position (shown in the covariance topic).

Another way to see it. Ask "which one is more useful?". A printer that handles more kinds of input is more useful, so it is the subtype (it can stand in for the less capable one). For containers you read from, the one that promises a more specific content is more useful. Subtype = "can do at least everything the other one can".

The rule F[-A]: F[X] <: F[Y] iff Y <: X. The subtypes of Printer[Bird] are Printers of Bird or anything above it (Animal, Any), plus Nothing.
Trap Printer[Nothing] is not a subtype of Printer[D]: it would need D <: Nothing. (Plain Nothing is.) Also Printer[E] for a subclass E of D fails: it cannot print an arbitrary D. With traits: if class D extends A with B, then Printer[A], Printer[B] and Printer[Any] are all accepted where a Printer[D] is wanted.
On the exam "Which calls do not give a type error" with a contravariant parameter: read it as "which printers can handle every value of the required type". Then list the type and all its ancestors (including traits); those are the answers.
Try it: subtypes of Printer[Bird]

Printer[Animal], Printer[Any], Printer[Parrot], Printer[Nothing]?

Answer: Printer[Animal] and Printer[Any]. Parrot and Nothing are below Bird, which is the wrong direction for -. Compiler-checked.

Try it: double flip

Is Printer[Printer[Parrot]] <: Printer[Printer[Bird]]? Is Printer[Box[Parrot]] <: Printer[Box[Bird]]? Is Printer[Any] <: Printer[Box[Bird]]?

Answer: yes, no, yes.

(1) Outer -: need Printer[Bird] <: Printer[Parrot]. Inner -: need Parrot <: Bird: true. Two flips cancel. (2) Outer -: need Box[Bird] <: Box[Parrot]; Box +: need Bird <: Parrot: false. (3) need Box[Bird] <: Any: true. All three checked with the compiler.

Functions: contravariant in the argument, covariant in the result

A function type A => B is the trait Function1[-A, +B]. The slide rule:

A => B <: C => D iff C <: A and B <: D.

Why: somebody who expects a C => D will call your function with a C and use the answer as a D. Your function is a safe replacement if it accepts at least every C (its argument type A is C or wider) and it returns something that is a D (its result type B is D or narrower). "Write to arguments, read from the result."

class Pokemon { override def toString = "Pokemon" }
class Pikachu extends Pokemon { override def toString = "Pikachu" }
class Raichu extends Pikachu { override def toString = "Raichu" }
def pokeTransform(f: Pikachu => Pokemon, p: Pikachu): Pokemon = f(p)
val evolve: Pikachu => Raichu = p => new Raichu    // narrower result: fine
val stay: Pokemon => Pokemon = p => p              // wider argument: fine
val anything: Any => Raichu = x => new Raichu      // both at once: fine
println(pokeTransform(evolve, new Pikachu))
println(pokeTransform(stay, new Pikachu))
println(pokeTransform(anything, new Pikachu))
Raichu
Pikachu
Raichu

The slide's counterexample: a function that only accepts Raichus cannot replace a Pikachu => Pokemon, because pokeTransform will feed it a plain Pikachu.

class Pokemon; class Pikachu extends Pokemon; class Raichu extends Pikachu
def pokeTransform(f: Pikachu => Pokemon, p: Pikachu): Pokemon = f(p)
val wrong: Raichu => Pokemon = r => new Pikachu   // only accepts Raichus
pokeTransform(wrong, new Pikachu)
error: type mismatch;
 found   : Raichu => Pokemon
 required: Pikachu => Pokemon

Checklist for "which of these are subtypes of C => D"

  1. Argument of the option: must be C or above (C, its parents, Any). Nothing or a subclass of C as argument: reject.
  2. Result of the option: must be D or below (D, its subclasses, Nothing). Any or a parent of D as result: reject.
  3. If the argument or result is itself generic (List[Bird], Printer[Bird], MutVar[Bird]), the direction from step 1 or 2 is what you need for that whole type; then apply the variance method to it.

So the "best" possible function type is Any => Nothing: it is a subtype of every one-argument function type. The "worst" is Nothing => Any.

Worked example with layers

class List[+A], class Printer[-A], class MutVar[A], Animal > Bird > Ostrich. Which are subtypes of Printer[Bird] => List[Bird]?

OptionArgument: need Printer[Bird] <: argResult: need res <: List[Bird]OK?
Printer[Ostrich] => List[Ostrich]Printer flips: need Ostrich <: Bird: yesList keeps: Ostrich <: Bird: yesyes
Printer[Animal] => List[Ostrich]need Animal <: Bird: noyesno
Printer[Ostrich] => List[Animal]yesneed Animal <: Bird: nono
Printer[Nothing] => List[Nothing]need Nothing <: Bird: yesyesyes
Any => Nothingyesyesyes

Notice how Printer[Ostrich] is a fine argument type here: the function flips once, Printer flips again, so in the end the inside goes the same way as the class hierarchy. Counting flips is a fast check: an even number of - layers above a spot means "same direction", odd means "opposite", and an invariant layer means "equal".

The lub of function types

A list of functions gets the most specific function type that all of them fit. By the rule, that is: argument = the most specific common subtype of the arguments (the "lowest" one, glb), result = the lub of the results.

class Animal; class Bird extends Animal; class Parrot extends Bird
val f: Bird => Parrot = b => new Parrot
val g: Parrot => Bird = p => p
val h: Animal => Animal = a => a
val t1: Int = List(f, g, h)   // deliberately wrong: the error shows the inferred type
val t2: Int = List(f, h)
error: type mismatch;
 found   : List[Parrot => Animal]
 required: Int
error: type mismatch;
 found   : List[Bird => Animal]
 required: Int

Arguments Bird, Parrot, Animal: the one below all of them is Parrot. Results Parrot, Bird, Animal: the lub is Animal. Hence List[Parrot => Animal]. Not Animal => Animal: g only accepts Parrots, so the list cannot promise to accept any Animal.

Nothing as a parameter type

A parameter of type Nothing accepts no ordinary value, because no value has type Nothing. Only an expression that never returns, like throw, has type Nothing:

class Animal; class Bird extends Animal; class Parrot extends Bird
def onlyNothing(x: Nothing): Int = 3
def boom: Nothing = throw new Error("boom")
println(scala.util.Try(onlyNothing(boom)))   // compiles; the throw happens before the call
Failure(java.lang.Error: boom)
error: type mismatch;
 found   : Parrot
 required: Nothing
The rule A => B <: C => D iff C <: A and B <: D. Argument may get wider, result may get narrower. Any => Nothing is below every function type.
Trap Treating both sides covariantly: Dog => Dog is not a subtype of Mammal => Mammal. Slide: "Labrador => Labrador <: Dog => Dog: incorrect; Animal => Labrador <: Dog => Dog: correct". Accepting Any => Any because its input is broad: its result is too broad. Rejecting X => Nothing results: Nothing is below every result type.
Trap A function type inside a contravariant class flips once more. With class SetFree[-A] and a parameter SetFree[Mammal => Mammal], you need (Mammal => Mammal) <: X, so X must be a supertype of Mammal => Mammal: argument Mammal or below, result Mammal or above. Capybara => Animal works, Animal => Capybara does not.
On the exam Patterns: "which are subtypes of Dog => Animal", "which calls to f(g: X => Y) type-check", "most specific type of List(f, g, h)", and calls like b[A,B](lambda, x). For the last: first fill in the explicit type arguments, so the parameter becomes a concrete function type, then check the lambda's type against it with the rule, and check the other arguments too (a parameter of type Nothing only takes a throw).

Worked example of the b[A,B](lambda, x) pattern:

class Animal; class Bird extends Animal
def b[X, Y](f: X => Y, z: Any): Nothing = throw new Error("never returns")
def ok1 = b[Bird, Animal]((x: Animal) => x, false)
def bad = b[Animal, Animal]((x: Bird) => x, "s")
error: type mismatch;
 found   : Bird => Bird
 required: Animal => Animal

ok1: filling in X = Bird, Y = Animal makes the parameter Bird => Animal. The lambda (x: Animal) => x has type Animal => Animal: argument Animal is wider than Bird (good), result Animal equals Animal (good). Compiles. bad: the parameter is Animal => Animal, the lambda only accepts Birds: rejected. The second argument has type Any, so false and "s" are both fine there.

Try it: subtypes of Bird => Bird

Animal => Parrot, Parrot => Parrot, Any => Nothing, Bird => Animal, Nothing => Any?

Answer: Animal => Parrot and Any => Nothing. Parrot => Parrot: argument too narrow. Bird => Animal: result too wide. Nothing => Any: both wrong. Compiler-checked.

Try it: containers inside a function

Box is +, Printer is -. Which are subtypes of Box[Bird] => Printer[Bird]? (a) Box[Animal] => Printer[Animal] (b) Box[Parrot] => Printer[Animal] (c) Box[Any] => Printer[Parrot] (d) Any => Nothing

Answer: a and d.

Argument: need Box[Bird] <: arg, so Box[X] with X Bird or above: a and c pass, b fails. Result: need res <: Printer[Bird]; Printer flips, so Printer[Y] with Y Bird or above: a passes, c fails (Parrot). d passes trivially. Compiler-checked.

Try it: an invariant argument

Cell is invariant, Box is +. Which are subtypes of Cell[Bird] => Box[Bird]? (a) Cell[Animal] => Box[Parrot] (b) Cell[Bird] => Box[Parrot] (c) Any => Box[Nothing]

Answer: b and c. The argument must be a supertype of Cell[Bird]: that is Cell[Bird] itself or Any/AnyRef, never Cell[Animal]. Results: Box[Parrot] and Box[Nothing] are both below Box[Bird]. Compiler-checked.

Try it: a function type inside a Printer

Which are subtypes of Printer[Bird => Bird]? (a) Printer[Parrot => Animal] (b) Printer[Animal => Parrot] (c) Printer[Bird => Any]

Answer: a and c. Printer flips: we need (Bird => Bird) <: X, so X is a supertype of Bird => Bird: argument Bird or below, result Bird or above. (a) Parrot below, Animal above: yes. (b) is a subtype of Bird => Bird, the wrong direction. (c) yes. Compiler-checked.

Type bounds: <: and >:

A type bound puts a condition on a type parameter. Slide: "Type bounds give extra constraints on type arguments".

  • Upper bound [A <: Bird]: A may be Bird or any subtype of Bird. Inside the method you may use everything a Bird has.
  • Lower bound [B >: A]: B may be A or any supertype of A.

Upper bound: a restriction, not the resulting type

Compare a generic method with a bound and a plain method:

import scala.reflect.runtime.universe._
def tpe[T: WeakTypeTag](x: T) = weakTypeOf[T].toString.replaceAll("""\$anon\.this\.|this\.""", "")
class Animal; class Bird extends Animal; class Parrot extends Bird
def keep[A <: Bird](x: A): A = x         // upper bound: A must be some kind of Bird
def widen(x: Bird): Bird = x             // plain parameter of type Bird
def make(x: Bird): Parrot = new Parrot
def same[A](x: A): A = x                 // no bound at all
println(tpe((keep(new Parrot), widen(new Parrot), keep(widen(new Parrot)),
             same(make(new Bird)), widen(keep(new Parrot)))))
(Parrot, Bird, Bird, Parrot, Bird)

(tpe is a small helper that prints the static type the compiler inferred.) Trace each component:

ExpressionStatic typeWhy
keep(new Parrot)ParrotA is inferred as Parrot (allowed: Parrot <: Bird), result type A
widen(new Parrot)Birddeclared result type Bird; the Parrot is forgotten
keep(widen(new Parrot))Birdinner already Bird, so A = Bird
same(make(new Bird))Parrotmake returns Parrot; same keeps it
widen(keep(new Parrot))Birdthe outer function decides: Bird

Work from the innermost call outward, and at each step use the declared result type of the method, with A replaced by what was inferred. The runtime object (a Parrot every time here) does not matter; the static type is what the question asks.

A type argument that breaks the bound is a compile error:

class Animal; class Bird extends Animal
def keep[A <: Bird](x: A): A = x
keep("hello")
keep(new Animal)
error: inferred type arguments [String] do not conform to method keep's type parameter bounds [A <: Bird]
keep("hello")

Bounds that mention the parameter itself

The slides use [A <: Ordered[A]]: "A must be something that can be compared with another A". This lets a generic method call < or compareTo on its arguments.

trait Comparable[A] {
  def compareTo(rhs: A): Int
  def <(rhs: A): Boolean = compareTo(rhs) < 0
}
case class Money(cents: Int) extends Comparable[Money] {
  def compareTo(rhs: Money): Int = cents - rhs.cents
}
case class Date(y: Int, m: Int) extends Comparable[Date] {
  def compareTo(rhs: Date): Int = if (y == rhs.y) m - rhs.m else y - rhs.y
}
case class Pair[A <: Comparable[A], B <: Comparable[B]](a: A, b: B) extends Comparable[Pair[A, B]] {
  def compareTo(rhs: Pair[A, B]): Int = {
    val c = a.compareTo(rhs.a)
    if (c != 0) c else b.compareTo(rhs.b)
  }
}
def bigger[A <: Comparable[A]](x: A, y: A): A = if (x < y) y else x
println(bigger(Money(250), Money(199)))
println(Pair(Date(2024, 3), Money(10)).compareTo(Pair(Date(2024, 7), Money(99))))
println(Pair(Date(2024, 3), Money(10)).compareTo(Pair(Date(2024, 3), Money(99))))
val p1 = Pair(Date(2025, 1), Money(5))
val p2 = Pair(Date(2023, 12), Money(500))
println(if (p1 < p2) p1.compareTo(p2) else p2.compareTo(p1))
Money(250)
-4
-89
-2

Trace the three Pair lines (first component decides, unless it is 0):

Calla.compareToResult
Pair(Date(2024,3), Money(10)) vs Pair(Date(2024,7), Money(99))same year: 3 - 7 = -4-4 (not 0, Money ignored)
Pair(Date(2024,3), Money(10)) vs Pair(Date(2024,3), Money(99))3 - 3 = 0Money: 10 - 99 = -89
p1 < p2?2025 - 2023 = 2, not < 0, so falseelse branch: p2.compareTo(p1) = 2023 - 2025 = -2

A field typed by a bound parameter keeps its type

trait Comparable[A] { def compareTo(rhs: A): Int }
case class Money(cents: Int) extends Comparable[Money] {
  def compareTo(rhs: Money): Int = cents - rhs.cents
}
case class Holder(c: Comparable[Money])          // field type fixed to the trait
case class GHolder[A <: Comparable[A]](c: A)     // field type is the type parameter
val show: Int = (Holder(Money(1)).c, GHolder(Money(1)).c)   // deliberately wrong, shows the type
error: type mismatch;
 found   : (Comparable[Money], Money)
 required: Int

Holder's field has the fixed type Comparable[Money]; GHolder's field has type A, which was inferred as Money.

Lower bound: how a covariant class can still take arguments

We saw that class Box[+A] { def put(y: A) } does not compile. A covariant immutable list still wants a "prepend" method. The fix: let the method choose a new element type B that is A or above, and return a list of B.

class Animal; class Bird extends Animal; class Parrot extends Bird; class Fish extends Animal
sealed abstract class MyList[+A] {
  def prepend[B >: A](x: B): MyList[B] = Cons(x, this)
}
case class Cons[A](head: A, tail: MyList[A]) extends MyList[A]
case object Empty extends MyList[Nothing]
val ps: MyList[Parrot] = Empty.prepend(new Parrot)
// each line below is deliberately wrong, so the compiler prints the inferred type
val t1: Int = ps.prepend(new Parrot)
val t2: Int = ps.prepend(new Bird)
val t3: Int = ps.prepend(new Fish)
val t4: Int = ps.prepend("hello")
val t5: Int = ps.prepend(42)
error: type mismatch;
 found   : MyList[Parrot]
 required: Int
val t1: Int = ps.prepend(new Parrot)
                        ^
error: type mismatch;
 found   : MyList[Bird]
 required: Int
val t2: Int = ps.prepend(new Bird)
                        ^
error: type mismatch;
 found   : MyList[Animal]
 required: Int
val t3: Int = ps.prepend(new Fish)
                        ^
error: type mismatch;
 found   : MyList[Object]
 required: Int
val t4: Int = ps.prepend("hello")
                        ^
error: type mismatch;
 found   : MyList[Any]
 required: Int
val t5: Int = ps.prepend(42)
                        ^
Call on MyList[Parrot]B must be >: Parrot and fit the argumentResult
prepend(new Parrot)ParrotMyList[Parrot]
prepend(new Bird)lub(Parrot, Bird) = BirdMyList[Bird]
prepend(new Fish)lub(Parrot, Fish) = AnimalMyList[Animal]
prepend("hello")lub(Parrot, String) = AnyRef (printed as Object)MyList[Object]
prepend(42)lub(Parrot, Int) = Any (Int is a value type)MyList[Any]

Why this is safe: the method never puts a Fish into a list of Parrots. It makes a new list whose type is wide enough for both. This is exactly how Scala's own ::, +: and updated are declared, which is why new Cat :: dogs gave List[Animal].

Bounds in other languages (slide)

Java: variance is declared at use, arrays covariant, type bounds similar. C++: templates are always invariant, type bounds are implicit ("duck typed"). Haskell: no subtyping, so no variance; bounds are on type classes.

The rule [A <: T]: A is T or below, and the method's result keeps the specific A that was inferred. [B >: A]: B is A or above; used so a covariant class can accept new elements by returning a wider type.
Trap Thinking the upper bound becomes the type: GumpBox[A <: Springy](c: A) holding a Kangaroo gives c: Kangaroo, while JumpBox(c: Springy) gives c: Springy. Thinking the runtime class survives a plain method: fid(x: Citrus): Citrus returns type Citrus even if you passed a Lemon. Thinking compareTo returns only -1, 0 or 1: it returns whatever the subtraction gives.
On the exam "Most specific type of v/t" with nested calls: go inside out, write the type after every call. "Which statements are true of def sort[A <: Ordered[A]]": it works for any type A that has the Ordered[A] trait. Value questions with Comparable: compute compareTo literally, watch which side is this and which is rhs, and follow the if/else.
Try it: what are the static types?

With keep, widen, make, same as above: what is the type of (same(widen(new Parrot)), keep(make(new Bird)))?

Answer: (Bird, Parrot). widen returns Bird and same keeps Bird. make returns Parrot and keep infers A = Parrot.

(Bird, Parrot)
Try it: compareTo values

With the code above, what is Pair(Date(2023, 5), Money(7)).compareTo(Pair(Date(2023, 5), Money(3)))? And Pair(Date(2022, 9), Money(7)).compareTo(Pair(Date(2023, 1), Money(3)))?

Answer: 4 and -1. First: dates equal (0), so Money: 7 - 3 = 4. Second: years differ, so 2022 - 2023 = -1; Money is never looked at.

println(Pair(Date(2023, 5), Money(7)).compareTo(Pair(Date(2023, 5), Money(3))))
println(Pair(Date(2022, 9), Money(7)).compareTo(Pair(Date(2023, 1), Money(3))))
4
-1
Try it: prepend

val ps: MyList[Parrot]. What is the type of ps.prepend(new Bird).prepend(new Parrot)?

Answer: MyList[Bird]. After the first call the list is a MyList[Bird]. Prepending a Parrot to it: B must be >: Bird and fit a Parrot, so B = Bird. Lists never get narrower by adding.

error: type mismatch;
 found   : MyList[Bird]
 required: Int

Recap: traits, variance and bounds in one screen

Question typeMethod
Which method body runs?Linearization row: class, then built from the right, shared ancestors last. First body in the row wins. The class's own definition beats all traits.
Diamond extends B with CC's version (last trait wins). A body in A calling a still uses the object's a.
super.m in a traitNext body to the right in the object's row; may jump to a sibling trait. Build the result like a stack.
Two unrelated traits, same concrete methodCompile error "inherits conflicting members" unless the class overrides.
Which types does List(a, b, c) have?Intersect the ancestor sets. List[T] for each shared T, plus List[Any] and Any. Never Nothing.
F[+A]F[X] <: F[Y] iff X <: Y. Read-only, immutable. List, Option, tuples.
F[-A]F[X] <: F[Y] iff Y <: X. Write-only (Printer).
F[A]Only equal. Array, var fields, mutable classes.
A => B <: C => DC <: A and B <: D. Best: Any => Nothing.
Nested typesOutside in; each - flips, invariant demands equality.
lub of functionsglb of arguments => lub of results.
Nothing / Null / AnyNothing below all (always a subtype answer); Null below all reference types; Any above all.
[A <: T]A is T or below; result keeps the inferred A. Violating it: "do not conform to ... bounds".
[B >: A]Lets covariant F[+A] take a value and return F[lub].
Variance errors"covariant type A occurs in contravariant position" = a +A in an argument or var; the reverse for -A in a result.

Week 7: The programming language zoo

The last lecture compares twelve languages on the same few properties: typing, garbage collection, objects, traits, pattern matching, mutability of the standard library, first class functions and generics. Every exam has one or two multiple response questions of the form "which statements about X are true" or "in which language is this code written". These are pure facts plus a few concepts, so a good table and a list of syntax clues give you sure points.

Popularity indexes: TIOBE and PYPL

How do you measure how "popular" a language is? The slides name three rankings:

IndexBased on
TIOBE Programming language indexthe number of search engine results (how many pages mention the language)
PYPL (PopularitY of Programming Languages)the number of search engine searches (how often people search for it, for example for tutorials)
IEEE Spectrum rankinga combination of several sources

Positions on the slides (they change every year, but this is what the course shows):

LanguageTIOBEPYPL
Python#1#1
C#2#3
C++#3#3
Java#4#2
C##5#11
JavaScript#6#6
Rust#10#8
PHP#14#7
Kotlin#26#19
Lisp#43n/a
Scala#44#22
Haskell#49#27
The rule TIOBE counts search results, PYPL counts searches. Top 5 on TIOBE (slides): Python, C, C++, Java, C#. Scala, Haskell and Lisp are far down.
Trap "Scala is in the top 5 of TIOBE" is false: the course language is a well-known JVM language, but on the slide it is #44. Do not mix up results (TIOBE) and searches (PYPL).
On the exam Usually one option inside a bigger "which statements are true" question. Know the two definitions and roughly who is on top.
Try it: true or false?
  1. PYPL ranks languages by the number of search engine results.
  2. Python is #1 on both TIOBE and PYPL on the slides.
  3. Haskell is in the TIOBE top 10.

Answer: only b. (a) That is TIOBE; PYPL uses searches. (c) Haskell is #49.

Try it: which index?

A ranking counts how often people type "Kotlin tutorial" into Google. Which index works like that?

Answer: PYPL: it counts searches. TIOBE would count how many pages about Kotlin exist.

The properties, and the big comparison table

Every language slide has the same table. First what each row means, because the exam options test the concepts as much as the facts.

Typing: static or dynamic
When are types checked? Static: at compile time, before the program runs; a type error means the program is not run at all. Dynamic: while the program runs; a type error happens only when that exact line is executed. A variable in a dynamic language can hold an int now and a string later.
Type inference
The compiler works out types you did not write (val x = 5 in Scala, var in Java 10 and C#, everything in Haskell). That is still static typing. Not writing types is not the same as dynamic typing.
Strong or weak (not on the slides)
Whether the language silently converts between unrelated types. JavaScript gives "5" + 2 = "52" and "5" * 2 = 10 (weak); Python refuses "5" + 2 with a TypeError (strong, but dynamic). C lets you cast and do pointer arithmetic freely (weak, but static). Static/dynamic and strong/weak are two separate questions.
Garbage collection (GC)
The runtime finds objects nobody refers to any more and frees their memory. Without GC (C, C++) you free memory yourself, which can lead to crashes and security bugs. Rust has no GC but checks memory use at compile time with ownership types. Memory safety does not require GC.
Compiled, interpreted, virtual machine (general knowledge)
C, C++, Rust, Haskell compile to machine code. Java, Scala, Kotlin compile to JVM bytecode, which the Java Virtual Machine runs (portable, with a JIT compiler that turns hot code into machine code at runtime); C# does the same on the .NET runtime. Python, PHP and JavaScript are normally run by an interpreter or a JIT engine (JavaScript in the browser).
Objects, subtyping, inheritance
Classes, objects, and "a Dog is an Animal". Haskell and Rust have none of it; JavaScript has objects but uses prototypes instead of classes (slide: "Yes/No").
Traits
Scala-like traits or interfaces with default methods. Python and C++ instead allow multiple inheritance of classes; Haskell and Rust have type classes / Rust traits ("Yes/No").
Pattern matching
match with case and deconstruction. On the slides only Scala, Haskell, Rust (and Lean) have it.
Default API mutability
Are the standard library's data structures mutable (changed in place) or immutable? Mutable everywhere except Haskell (immutable), Clojure (immutable) and Scala (both).
First class functions
Functions are values: you can pass them as arguments, return them and store them, with closures. C only has function pointers without closures, so "No".
Parametric polymorphism
Generics like List[A]. Dynamic languages do not need them (slide: "No (dynamically typed)"); C fakes them with macros.

A dynamic typing demo, Python versus Scala (same mistake, 1 + "a" in a branch that runs only when flag is true):

def f(flag):
    if flag:
        return 1 + "a"   # a type error, but only if this line runs
    return "fine"

print(f(False))
print(f(True))
fine
Traceback (most recent call last):
  ...
TypeError: unsupported operand type(s) for +: 'int' and 'str'
def f(flag: Boolean): String =
  if (flag) 1 - "a"
  else "fine"
println(f(false))
error: overloaded method - with alternatives: ... cannot be applied to (String)

Python ran the program, printed fine, and crashed only when it reached the bad line. Scala refused to run anything. So in a dynamic language type errors occur only at runtime, and programs can certainly crash.

Table 1: the slide rows

LanguageYearTypingGCObjects, subtyping, inheritanceTraitsPattern matchingDefault APIFirst class functionsGenerics
Java1995staticyesyesyes (default implementations, new)nomutableyes (new)yes
C1972staticnonononomutableno (function pointers, no closures)no (macros)
C++1985staticnoyesno (multiple inheritance)nomutableyes (new)yes (templates)
Python1990dynamicyesyesno (multiple inheritance)nomutableyesno (dynamically typed)
C#2000staticyesyesyes (C# 8)nomutableyesyes
PHP1995dynamicyes (newish)yesnonomutableyesno (dynamic)
JavaScript1995dynamicyesyes/no (prototype based)nonomutableyesno (dynamic)
Scala2004staticyesyesyesyesimmutable and mutableyesyes
Haskell1990staticyesnoyes/no (type classes)yesimmutableyesyes
Kotlin2011staticyesyesyesnomutableyesyes
Rust2010staticno (ownership)noyes/no (type classes)yesmutableyesyes
Lisp family1958dynamicyesno (library)nono (library)mutable (Common Lisp, Scheme), immutable (Clojure)yesno

Table 2: what it is used for and how it runs

LanguageUsed most in (slides)Famous software (slides)Known for (slides)Runs asStrong/weakMemory safe
Javaweb back ends, Android apps, businessGmail back end, Minecraft (original)JVM platform (portable, JIT), a bit verboseJVM bytecodestrongyes
Csystems, embedded, last bit of performanceDOOM, Linux/Unix/Windows/macOS kernels, Gitsmall language, low levelmachine codeweakno
C++systems, embedded, performance, gamesbrowsers (IE, Chrome, Safari), Unity, Unreal, most AAA gameslow level (higher than C), complexmachine codeweakno
Pythondata analysis, machine learning, server side webYouTube, Instagram, AlphaGo (TensorFlow)slow, easy start, whitespace indentation, made at CWI (Amsterdam)interpreterstrongyes
C#business, web, games (Unity)Stack Overflow, banks, indie games, CupheadMicrosoft, very similar to Java.NET bytecodestrongyes
PHPweb back endFacebook, Wikipediamixed with HTML, "badly designed by many", meant for non-programmersinterpreter on the web serverweakyes
JavaScriptweb front endany interactive webpageonly language for client side web, nothing to do with Java, made in 10 days, wartsbrowser engine (interpreter + JIT)weakyes
Scalafinance, web (Play)X/Twitter, Coursera, NY Times, Guardianmixing OO and functional, complexJVM bytecodestrongyes
Haskellfinance, language researchFacebook spam filter (Haxl), Standard Chartered, GHCinfluential, lazy evaluation, type inference, type classes (monads)machine code (GHC)strongyes
KotlinAndroid, bankingPinterest and Evernote Android appsnew JVM language, Google's preferred Android language, null safety (String?)JVM bytecodestrongyes
Rustsystems programmingFirefox reimplementation (Servo), Libraownership types instead of manual memory management, functional systems programmingmachine codestrongyes (without GC)
Lisp(past) language and AI research, web back end (Clojure)Emacs Lisp, Walmart data systemminimal, code is data (homoiconic), macros, very influential (SICP)interpreter/REPL or compiled; Clojure on the JVMstrongyes

The columns "Runs as", "Strong/weak" and "Memory safe" are standard background, not slide rows. Strong/weak is a fuzzy term, so the exam only uses it in obvious cases.

New on the 2026 slides: Lean (2013). Used for interactive theorem proving, formalizing mathematics and software/hardware verification (Mathlib). Static, garbage collection by reference counting, no objects/subtyping, type classes ("yes/no" traits), pattern matching yes, immutable API, first class functions and generics yes. Known for: functional programming and theorem proving in one language, total (does not allow non-terminating computations), and its role in training LLMs on mathematical reasoning. Syntax: def f (n : Nat) : Bool :=, #eval, theorem ... := by.

The rule Dynamic: Python, PHP, JavaScript, Lisp. Everything else static. No GC: C, C++, Rust. No objects/subtyping: C, Haskell, Rust (and Lisp without a library). Pattern matching: Scala, Haskell, Rust. Immutable API: Haskell (and Clojure, and Scala partly). No generics: C, Python, PHP, JavaScript, Lisp.
Trap Dynamic typing says nothing about GC, inheritance or crashes. "Dynamically typed languages have no GC / no inheritance / never crash" are all false; the only true consequence is "type errors occur only at runtime". And type inference (Haskell, Scala, C# var) is not dynamic typing.
Trap No mainstream language on the list except Haskell has "a type system to separate pure and impure functions" (Haskell marks side effects with IO in the type). Options claiming that for Java, C# or JavaScript are false. "All functions are pure" is false for every language here except Haskell.
On the exam For "which statements are true": take each statement separately, find the language's row, and only tick what the table says. Watch for statements that mix two languages (Java vs JavaScript, C vs C++) and for "all", "only", "never".
Try it: dynamically typed languages

Which have dynamic typing: Haskell, JavaScript, Python, C, PHP, C++, Java, C#?

Answer: JavaScript, Python, PHP. Haskell infers types but checks them at compile time; C allows casts but is static; C# var is inference.

Try it: garbage collection

Which use garbage collection: C, Java, Rust, Python, Lisp, C#, C++, JavaScript?

Answer: Java, Python, Lisp, C#, JavaScript. C and C++ free memory manually (C++ destructors and smart pointers are not a garbage collector). Rust is memory safe through ownership, without GC.

Try it: generics

Which support parametric polymorphism (generics): Python, C, Java, C#, Scala, PHP?

Answer: Java, C#, Scala. Python and PHP are dynamically typed, so the slide says no. C uses macros and void*, which is not language-level generics.

Java (1995)

Static, garbage collected, object oriented with subtyping and inheritance, interfaces with default implementations (new), no pattern matching (slide), mutable standard library, first class functions (lambdas, new), generics. Made by Sun, now Oracle. Java source is compiled to JVM bytecode, not directly to x86 machine code; the JVM makes it portable and JIT-compiles it. Used for web back ends, Android apps and business software; Gmail back end and the original Minecraft. Known as a bit verbose (FoobarFactoryFacadeImplementation). Arrays are covariant (the "mistake" from week 6).

Spot it: public class X, public static void main(String[] args), System.out.println, types before names (long sum = 0), new ArrayList<>(), semicolons and braces.

import java.util.ArrayList;
import java.util.List;

public class Shelter {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>();
        names.add("Rex");
        for (String n : names) {
            System.out.println("Hello " + n);
        }
    }
}
Trap "Java files are directly compiled to x86 binaries": false (bytecode). "Java is dynamically typed": false; runtime type information and reflection do not make it dynamic. "All Java code is valid JavaScript": false; the names are similar for marketing reasons only.
Try it: true or false?
  1. Java supports inheritance.
  2. Java has subtyping.
  3. Java has a type system that separates pure from impure functions.
  4. Java is statically typed.

Answer: a, b, d true; c false (that is Haskell's IO).

Try it: Java or C#?
public class Hi { public static void main(String[] args) { System.out.println("hi"); } }

Answer: Java: System.out.println, lower-case main, String[] args. C# would write static void Main(string[] args) and Console.WriteLine.

C (1972)

Static, no garbage collection (you call malloc and free yourself), no objects, subtyping or inheritance, no traits, no pattern matching, mutable, no first class functions (only function pointers, no closures), no generics (use macros). A small, low-level language. Used for systems programming, embedded systems and squeezing out the last bit of performance. Famous: DOOM, the Linux/Unix/Windows/macOS kernels, Git. Most C code is also valid C++ code (C++ grew out of C).

Spot it: #include <stdio.h>, int main(), printf("%d\n", x), pointers with * and &, malloc/free, no classes, no std::, no template.

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *xs = malloc(3 * sizeof(int));
    for (int i = 0; i < 3; i++) xs[i] = i * i;
    printf("%d\n", xs[2]);
    free(xs);          /* you free memory yourself */
    return 0;
}
Trap "C is dynamically typed" because it lets you cast pointers: false, it is static (and weak). "The C standard library is immutable": false, C is built on mutable memory. "The Linux kernel is written in C": true.
Try it: true or false?
  1. C has closures.
  2. C supports parametric polymorphism.
  3. Rust is an alternative to C.
  4. Most C code is also C++ code.

Answer: c and d true. C has function pointers but no closures, and fakes generics with macros.

Try it: C or C++?
#include <stdio.h>
int main() { int sum = 0; for (int i = 1; i < 5; i++) sum += i; printf("%i\n", sum); return 0; }

Answer: C: stdio.h and printf, nothing C++-only. (It would also compile as C++, but C++ code on the exam shows std::cout, template, class.)

C++ (1985)

Static, no garbage collection, objects with subtyping and inheritance, no traits but multiple inheritance of classes (diamond: compiler error), no pattern matching, mutable, first class functions (lambdas, new), generics via templates (always invariant, bounds implicit, "duck typed"). Low level, but higher than C; a complex language. Used for systems, embedded, performance and games. Famous: web browsers (IE, Chrome, Safari), Unity and Unreal engines, most AAA games.

Spot it: #include <iostream>, std::cout << x, template<typename T>, class Rect : public Shape, virtual, ::, std::vector, std::unique_ptr.

#include <iostream>
#include <vector>

template<typename T>
T biggest(const std::vector<T>& v) {
    T best = v[0];
    for (const T& x : v) if (x > best) best = x;
    return best;
}

int main() {
    std::vector<int> v = {3, 9, 4};
    std::cout << biggest(v) << std::endl;
}
Trap "C++ features garbage collection": false. Destructors, RAII and unique_ptr free memory at a fixed moment; that is not a garbage collector. Seeing classes and inheritance and guessing Java: Java has no #include, no template<typename T> and no std::.
Try it: true or false?
  1. C++ is used for most AAA games.
  2. C++ has traits.
  3. C++ templates give parametric polymorphism.
  4. C++ is dynamically typed.

Answer: a and c true. C++ has multiple inheritance instead of traits; it is static.

Try it: which language?
class Dog : public Animal {
public:
    std::string speak() const override { return "Woof"; }
};

Answer: C++: : public Animal, public: sections, std::string, const override.

Python (1990)

Dynamic typing, garbage collection, classes and objects with inheritance, no traits (multiple inheritance), no pattern matching (slide), mutable standard library, first class functions, no generics (dynamically typed). Initially developed at the CWI in Amsterdam. Known for being slow, easy to start with, and using whitespace indentation instead of braces. Used most for data analysis/data science, machine learning and server side web. Famous: YouTube, Instagram, AlphaGo (TensorFlow). #1 on both TIOBE and PYPL.

Spot it: def f(n): with a colon and indentation, no braces, no types, print(...), range(1, n), ** for power, f"{x} text", '''docstrings''', lambda x: ..., import/from ... import.

def squares(n):
    """Return the first n squares."""
    return [i ** 2 for i in range(n)]

x = 5
x = "five"          # fine: types are only checked at runtime
print(f"{x}: {squares(5)}")
Trap "All functions are pure" and "the default API is (fully) immutable": false. "Python uses ownership types" (that is Rust) or "needs variance annotations": false. "Used to squeeze out the last bit of performance": false, it is known as slow. True: dynamic typing, GC, first class functions, classes and objects, data science.
Try it: true or false?
  1. Python has first class functions.
  2. Python has classes and objects.
  3. Python supports parametric polymorphism (according to the slides).
  4. Python is often used in data science.

Answer: a, b, d true. c false: the slide says "No (dynamically typed)".

Try it: which language?
def total(xs):
    s = 0
    for x in xs:
        s += x
    return s
print(total([1, 2, 3]))

Answer: Python: def ...: plus indentation, no types, no braces. Scala also uses def, but writes def total(xs: List[Int]): Int = ....

C# (2000)

Developed by Microsoft, very similar to Java (differences in details). Static, garbage collected, objects with subtyping and inheritance, traits since C# 8 (default interface methods), no pattern matching (slide), mutable standard library, first class functions, generics. Like Java, arrays are covariant. Used for business, web, and games through the Unity engine. Famous: Stack Overflow, banking software, indie games such as Cuphead.

Spot it: using System;, Console.WriteLine, static void Main(string[] args), method names with a capital (ToString(), Where, Sum), var, foreach (var x in xs), Func<char, int>, LINQ (Enumerable.Range(1, 10).Where(n => ...)), $"text {x}".

using System;
using System.Linq;

class Program {
    static void Main(string[] args) {
        var evens = Enumerable.Range(1, 10).Where(n => n % 2 == 0);
        foreach (var n in evens)
            Console.WriteLine($"even: {n}");
    }
}
Trap Thinking var (or dynamic) makes C# dynamically typed: var is type inference, C# is static. "C# has an immutable standard library": false. "C# has a type system to separate pure and impure functions": false.
Try it: true or false?
  1. C# has garbage collection.
  2. C# has subtyping.
  3. C# is often used for programming indie games.
  4. C# is dynamically typed.

Answer: a, b, c true; d false.

Try it: which language?
Func<int, int> twice = x => x * 2;
Console.WriteLine(twice(21));

Answer: C#: Func<int, int> and Console.WriteLine.

PHP (1995)

Dynamic typing, garbage collection ("newish"), objects with inheritance, no traits (slide), no pattern matching, mutable, first class functions, no generics. Used for the web back end: it runs on the server and produces web pages, typically interspersed with HTML. Famous: Facebook, Wikipedia. Originally intended for non-programmers, and considered badly designed by many.

Spot it: <?php ... ?>, every variable starts with $, echo, . to glue strings, function f($x), foreach ($xs as $k => $v), HTML around the code.

<ul>
<?php
$names = ["Ann", "Bob"];
foreach ($names as $i => $name) {
    echo "<li>" . $i . ": " . htmlspecialchars($name) . "</li>";
}
?>
</ul>
Trap PHP for PlayStation/Xbox games, embedded systems or microwave ovens: false, it needs a web server runtime. "PHP features an immutable standard library": false. "PHP code is also Rust code": nonsense, false.
Try it: true or false?
  1. PHP is dynamically typed.
  2. PHP is used most to produce web pages.
  3. PHP has generics.

Answer: a and b true; c false ("No (dynamic)").

Try it: which language?
function greet($name) { return "Hi " . $name; }
echo greet("Ann");

Answer: PHP: $name, string glue with ., echo. JavaScript would write name without $ and + for strings.

JavaScript (1995)

Dynamic typing, garbage collection, objects but prototype based ("yes/no" for subtyping and inheritance), no traits, no pattern matching, mutable, first class functions, no generics. Used for the web front end: it is the only language for client side web programming, so every interactive web page uses it. It has nothing to do with Java (the name was marketing), was originally developed in 10 days and has "some warts" (like "5" + 2 being "52").

Spot it: function f(x) { ... }, let/const/var without types, arrow functions x => x * 2, console.log, document.getElementById, template literals with backticks `value: ${v}`, typeof, ===, JSON.stringify.

const button = document.getElementById("go");
let clicks = 0;
button.addEventListener("click", () => {
  clicks = clicks + 1;
  console.log(`clicked ${clicks} times`);
});

Weak typing, run with Node:

console.log("5" + 2, "5" * 2, typeof 42)
52 10 number
Trap "JavaScript is statically typed", "similar to Java", "an extended version of Java": false. "Firefox, Safari and Chrome are mostly implemented in JavaScript": false (C++; they run JavaScript). "Used for operating systems / systems programming": false. "Has ownership types" or "a type system that ensures every function is pure": false.
Try it: true or false?
  1. JavaScript has garbage collection.
  2. JavaScript is used to produce interactive webpages.
  3. JavaScript is dynamically typed.
  4. JavaScript is often used for systems programming.

Answer: a, b, c true; d false.

Try it: which language?
let v = 42;
v = "now a string";
document.getElementById("out").textContent = `v is ${v}`;

Answer: JavaScript: let without a type, the same variable gets a number then a string (dynamic), document.getElementById (browser), backtick template literal.

Scala (2004)

The course language: mixes object oriented and functional programming. Static, garbage collected (it runs on the JVM), objects with subtyping and inheritance, traits, pattern matching, both immutable and mutable collections (immutable is the default you import), first class functions, generics with variance. Complex language. Used in finance and web (Play framework). Famous: X/Twitter, Coursera, NY Times, the Guardian. Only #44 on TIOBE (slide).

Spot it: def f(x: Int): Int = (types after names, = before the body), val/var, object, case class, x match { case ... => }, square brackets for generics List[A], trait, extends ... with, _ placeholder.

case class Dog(name: String)
def describe(x: Any): String = x match {
  case Dog(n) => s"dog $n"
  case _      => "something else"
}
val dogs = List(Dog("Rex"), Dog("Fido")).map(describe)
Trap "In Scala all data is immutable" and "Scala only allows pure functions": false, you have var, Array and mutable collections. True: GC, first class functions. And Scala-specific facts from week 6 sometimes appear in this question: a final method cannot be overridden; a sealed class can still extend traits; an immutable data structure cannot be changed; you can pass any function value as an argument, not only nested functions.
Trap A purity question can appear here too. If f is pure: equal inputs give equal outputs; val x = f(a); val y = f(a) can be replaced by computing f(a) once and using it twice; and swapping a call of f with a call of some impure g does not change the result (f neither causes nor sees side effects). Purity says nothing about tail recursion.
Try it: true or false?
  1. Scala uses garbage collection.
  2. Scala has first class functions.
  3. Scala is in the TIOBE top 5.
  4. Scala has pattern matching.

Answer: a, b, d true; c false (#44).

Try it: Scala or Kotlin?
def isEven(n: Int): Boolean = n % 2 == 0

Answer: Scala: def and = before the body. Kotlin writes fun isEven(n: Int): Boolean = n % 2 == 0.

Haskell (1990)

A purely functional language. Static typing with full type inference, garbage collection, no objects, subtyping or inheritance (so no variance either), type classes instead of traits ("yes/no"), pattern matching, an immutable default API, first class functions, generics. Known for being influential, lazy evaluation (things are computed only when needed, so [1..], an infinite list, is fine), type inference and type classes (monads). Side effects are visible in the type (IO). Used in finance and language research; famous: Facebook's spam filter (Haxl), Standard Chartered trading software, the Glasgow Haskell Compiler.

Spot it: type signatures on their own line f :: Int -> [Int], definitions as equations with patterns f 0 = ... / f n = ..., where, data T = A | B, lambdas \x -> x * x, function calls without parentheses map f xs, backtick operators n `mod` 10, [1..], (x:xs).

data Shape = Circle Double | Square Double

area :: Shape -> Double
area (Circle r) = pi * r * r
area (Square s) = s * s

main :: IO ()
main = print (sum (map area [Circle 1, Square 2]))
Trap "Haskell is dynamically typed": false; you rarely write types because they are inferred, but they are checked at compile time. "Designed for object oriented programming", "used for AAA games", "used to implement Firefox": false. "The data structures in the Haskell API are all mutable": false. True: "Haskell does not have subtyping", "has pattern matching", "default API is immutable".
Try it: true or false?
  1. Haskell has subtyping.
  2. Haskell has pattern matching.
  3. The default API in Haskell is immutable.
  4. Haskell evaluates lazily.

Answer: b, c, d true; a false.

Try it: which language?
evens :: [Int]
evens = filter (\x -> x `mod` 2 == 0) [1..]

Answer: Haskell: the :: signature line, \x -> lambda, backtick mod, the infinite list [1..] (fine because of laziness).

Kotlin (2011)

A newer JVM language (introduced after 2000), endorsed by Google as the preferred Android language. Static, garbage collected, objects with subtyping and inheritance, traits (interfaces), no pattern matching (slide), mutable standard library, first class functions, generics. Its signature feature is null safety: only String? can contain null, a plain String cannot. Used for Android apps and banking; famous: the Pinterest and Evernote Android apps.

Spot it: fun main(), val/var (like Scala) but fun instead of def, nullable types String? and the Elvis operator ?:, string templates "$i" and "${u.name}", ranges for (i in 1..9), listOf(...), IntArray, data class.

data class User(val name: String, val email: String?)

fun main() {
    val users = listOf(User("Ann", null), User("Bob", "bob@x.nl"))
    for (u in users) {
        println("${u.name}: ${u.email ?: "no email"}")
    }
}
Trap "Not all object types can be null" is true for Kotlin (it is false for Java). "Kotlin has manual memory management": false, it is garbage collected. "The standard library is immutable": false; read-only interfaces like List are not the same as an immutable library.
Try it: true or false?
  1. Kotlin is often used to produce Android applications.
  2. Kotlin was introduced after 2000.
  3. Kotlin can run on the JVM.
  4. In Kotlin, every reference type can hold null.

Answer: a, b, c true; d false (only T? types).

Try it: which language?
fun main() {
    val name: String? = null
    println(name?.length ?: 0)
}

Answer: Kotlin: fun main(), the nullable type String?, safe call ?. and Elvis ?:.

Rust (2010)

A modern, safe alternative to C and C++ for systems programming. Static, no garbage collection: instead the compiler checks ownership types (every value has one owner; others may borrow it with &; memory is freed when the owner goes away), so you get memory safety without manual memory management and without GC. No objects/inheritance, traits that work like type classes ("yes/no"), pattern matching, first class functions, generics. "Functional systems programming". Famous: the Firefox reimplementation Servo, Libra.

Spot it: fn main(), let and let mut, borrows &v and types like &Vec<i32>, integer types i32, u64, usize, macros ending in ! (println!, vec!), closures |a, b| a + b, return type after ->, :: paths, .unwrap(), match.

fn longest(a: &String, b: &String) -> usize {
    if a.len() > b.len() { a.len() } else { b.len() }
}

fn main() {
    let mut names: Vec<String> = Vec::new();
    names.push(String::from("Ferris"));
    let n = longest(&names[0], &String::from("Crab"));
    println!("{}", n);
}
Trap "Rust has / uses / features garbage collection": false, every exam has a version of this. Memory safe does not mean garbage collected. "Rust is dynamically typed" (it infers types, but statically): false. "Invented before 1990", "an alternative to JavaScript", "used for interactive web pages": false. True: "an alternative to C", "used for systems programming", "uses ownership types".
Try it: true or false?
  1. Rust uses ownership types.
  2. Rust has garbage collection.
  3. Rust is often used for systems programming.
  4. Rust has pattern matching.

Answer: a, c, d true; b false.

Try it: which language?
let total: i32 = v.iter().fold(0, |a, &b| a + b);

Answer: Rust: let with i32, closure |a, &b| with a borrow pattern. fold alone proves nothing (Scala, Haskell and others have it).

The Lisp family (1958): Common Lisp, Scheme, Clojure

One of the oldest languages (1958; Scheme 1975, Common Lisp 1984, Clojure 2007). "No syntax": you write the abstract syntax tree directly as nested lists in parentheses, so the parser is very simple and code is data (homoiconic). That makes metaprogramming with macros natural. Dynamic typing, garbage collection (Lisp was the language where GC was invented), objects and pattern matching only through libraries, mutable (Common Lisp, Scheme) or immutable (Clojure), first class functions, no generics. Minimal and very influential (the book "Structure and Interpretation of Computer Programs"). Used in (past) language and AI research, and for web back ends (Clojure, on the JVM). Famous: Emacs Lisp, a Walmart data system. There were even "Lisp machines" that ran Lisp natively.

Spot it: parentheses everywhere, the operator comes first: (+ 1 2), (* n (fact (- n 1))). Common Lisp: (defun name (args) ...), (let ((x 1)) ...), '(), mapcar, (lambda (x) ...), (format t ...). Clojure: (defn name [args] ...) with square brackets, (ns ...), #(...), ->>.

(defun factorial (n)
  (if (<= n 1)
      1
      (* n (factorial (- n 1)))))

(print (mapcar #'factorial '(1 2 3 4 5)))
Trap "Lisp was introduced after 2000" (only Clojure is that new), "does not use a garbage collector", "often used for triple-A games", "an alternative to C++", "the Linux kernel is written in Common Lisp": all false. True: "in Lisp, code is data", "Lisp has garbage collection".
Try it: true or false?
  1. In LISP, code is data.
  2. LISP has garbage collection.
  3. LISP is statically typed.
  4. Clojure's default data structures are immutable.

Answer: a, b, d true; c false.

Try it: which language?
(defn square [x] (* x x))
(println (map square [1 2 3]))

Answer: Lisp, specifically Clojure: prefix calls in parentheses, defn with square-bracket parameters. Common Lisp would write (defun square (x) (* x x)).

Recap: spot the language, and the facts that get asked

Spot the language: go down this list, the first match wins

If you seeIt is
Code is mostly parentheses, operator first: (defun ...), (defn ...), (+ a b)Lisp (defun: Common Lisp; defn and [x]: Clojure)
<?php, variables with $, echo, HTML around itPHP
name :: Type -> Type lines, equations, \x ->, where, data X = A | B, [1..]Haskell
fn, let mut, & borrows, i32, println!, |x| closuresRust
fun, String?, ?:, "$x", for (i in 1..9)Kotlin
def f(x: Int): Int =, val, case class, match { case, List[A]Scala
def f(x):, indentation, no braces, no types, print(...), range, f"..."Python
#include with std::, cout <<, template<typename T>, class A : public BC++
#include <stdio.h>, printf, malloc, no classesC
Console.WriteLine, using System;, Main, Func<...>, LINQ .WhereC#
System.out.println, public static void main(String[] args)Java
function, let/const, console.log, document., backtick ${}, ===JavaScript

Do not decide on a single shared word: def (Python and Scala), let (JavaScript, Rust, Lisp, Haskell), fold/map/filter (almost everyone), braces and semicolons (C, C++, Java, C#, JavaScript, PHP, Rust). Look for the clue that only one language has.

The facts past exams asked, in one list

  • Dynamic: Python, PHP, JavaScript, Lisp. Static: Java, C, C++, C#, Scala, Haskell, Kotlin, Rust. Dynamic means type errors only at runtime, nothing else.
  • No GC: C, C++, Rust (ownership). GC: Java, Python, C#, PHP, JavaScript, Scala, Haskell, Kotlin, Lisp.
  • No subtyping: Haskell, Rust, C. Pattern matching: Scala, Haskell, Rust.
  • Immutable default API: Haskell, Clojure. Scala both. Everyone else mutable (including C's library).
  • Pure/impure in the type system: only Haskell. "All functions are pure" is false for the rest.
  • Generics: Java, C#, C++ (templates), Scala, Haskell, Kotlin, Rust. Not: C, Python, PHP, JavaScript, Lisp.
  • Before 1990: Lisp 1958, C 1972, C++ 1985. After 2000: C# 2000, Scala 2004, Rust 2010, Kotlin 2011.
  • Used for: Android (Kotlin, Java), interactive web pages (JavaScript), web pages from the server (PHP), data science (Python), systems (C, C++, Rust), AAA games (C++), indie games (C# via Unity), finance (Scala, Haskell).
  • JVM: Java, Scala, Kotlin, Clojure. Java compiles to bytecode, not to x86.
  • Relations: most C code is also C++; Rust is an alternative to C/C++; C# is very similar to Java; JavaScript has nothing to do with Java; Kotlin has null safety.

Practice: all 378 past exam questions

From the professor's OOFP Question Explorer (cs.vu.nl/~tcs/atze), with his explanations. Answer first, then check.

Mock exam

A full past exam in the real order, answers hidden until you hand it in. 135 minutes on the clock. Your answers count toward the "wrong before" filter in Practice.

2:15:00

Checklist: the official exam topic list

From the course's "Exam topic list" page. Rate yourself honestly; anything not green on Wednesday evening goes into Thursday morning.