Tutorial outline

Unit 1

  1. 01Getting started
  2. 02Names and functions
  3. 03Control
  4. 04Higher-order functions
  5. 05Environments and closures
  6. 06Functional abstraction
  7. 07Recursion
  8. 08Tree recursion

Unit 2

  1. 09Sequences
  2. 10Dictionaries, sets, and tuples
  3. 11Optionals
  4. 12Structures and enumerations
  5. 13Linked lists and trees
  6. 14Mutation and value semantics
  7. 15Classes and inheritance
  8. 16Protocols and generics
  9. 17Iterators and lazy sequences
  10. 18Efficiency and memoization

Unit 3

  1. 19Algebraic data types and pattern matching
  2. 20Writing an interpreter
  3. 21Error handling
  4. 22Concurrency with async and await
  5. 23Testing
  6. 24Building a Swift package

Functional abstraction

Generalizing with functions, iterative improvement and Newton’s method, contracts written as documentation and preconditions, and functions that wrap other functions.

CS61A
Abstraction · Function Examples · Composing Programs 1.6
Swift book
Functions · Closures

A function is an abstraction when you can use it without knowing how it works. max(3, 9) is 9; you have never needed to see its body. This lesson is about writing functions that others can use the same way: general enough to reuse, with a clear promise about what they do.

Generalizing

Lesson 4 turned three sums into one summation by making the varying part a parameter. The same function handles much less obvious terms. This sum converges, very slowly, to π:

8/(1·3) + 8/(5·7) + 8/(9·11) + ⋯
func summation(_ n: Int, _ term: (Int) -> Double) -> Double {
    var total = 0.0
    for k in 1...n {
        total += term(k)
    }
    return total
}

func piSum(_ n: Int) -> Double {
    summation(n) { k in 8.0 / Double((4 * k - 3) * (4 * k - 1)) }
}

print(piSum(1000))   // 3.141092653621038

summation now adds Doubles, and piSum only has to say what the kth term is.

Iterative improvement

Many numerical methods follow one pattern: start with a guess, and keep improving it until it is close enough. The pattern itself can be a function that takes the two varying parts, how to improve and how to judge, as arguments:

func improve(_ update: (Double) -> Double, _ close: (Double) -> Bool, _ guess: Double = 1) -> Double {
    var guess = guess
    while !close(guess) {
        guess = update(guess)
    }
    return guess
}

// The golden ratio is the positive number x with x * x == x + 1.
let golden = improve({ x in 1 / x + 1 }, { x in abs(x * x - (x + 1)) < 1e-10 })
print(golden)   // 1.6180339887802426

guess: Double = 1 gives the parameter a default value, so callers can leave it out.

Newton’s method is an improvement rule for square roots: a better guess for √a is the average of x and a / x.

func improve(_ update: (Double) -> Double, _ close: (Double) -> Bool, _ guess: Double = 1) -> Double {
    var guess = guess
    while !close(guess) { guess = update(guess) }
    return guess
}

func squareRoot(_ a: Double) -> Double {
    improve({ x in (x + a / x) / 2 }, { x in abs(x * x - a) < 1e-10 })
}

print(squareRoot(2))     // 1.4142135623746899
print(squareRoot(256))   // 16.00000000000039

The second result is not exactly 16: close accepts any guess whose square is within 10⁻¹⁰ of 256. Floating-point answers are compared with a tolerance, never with ==.

Put the lines in order

Put the lines in order. improve should keep updating the guess until close accepts it; the program prints true.

  1. }
  2. var guess = 1.0
  3. func improve(_ update: (Double) -> Double, _ close: (Double) -> Bool) -> Double {
  4. while !close(guess) {
  5. return guess
  6. guess = update(guess)
  7. let golden = improve({ 1 / $0 + 1 }, { abs($0 * $0 - ($0 + 1)) < 1e-10 })
  8. print(golden > 1.618 && golden < 1.619)
  9. }

Show the correct program
func improve(_ update: (Double) -> Double, _ close: (Double) -> Bool) -> Double {
    var guess = 1.0
    while !close(guess) {
        guess = update(guess)
    }
    return guess
}
let golden = improve({ 1 / $0 + 1 }, { abs($0 * $0 - ($0 + 1)) < 1e-10 })
print(golden > 1.618 && golden < 1.619)

A function’s contract

To use a function without reading its body, you need to know three things: what it takes, what it returns, and anything it assumes. Swift writes the first two in the signature. The rest belongs in a documentation comment, written with /// directly above the function:

/// Returns the number of ways to choose `k` items from `n`.
///
/// - Precondition: `0 <= k` and `k <= n`.
func choose(_ n: Int, _ k: Int) -> Int {
    precondition(0 <= k && k <= n, "k must be between 0 and n")
    var result = 1
    for i in 0..<k {
        result = result * (n - i) / (i + 1)
    }
    return result
}

print(choose(5, 2))   // 10

Xcode and other editors show these comments when you use the function. The precondition line turns the assumption into a check: if a caller breaks the contract, the program stops right there, instead of returning a meaningless number.

What would Swift print?

func half(_ n: Int) -> Int {
    precondition(n % 2 == 0, "n must be even")
    return n / 2
}
print(half(half(10)))

Type Error if running it would crash. For several lines of output, put each on its own line.

Show answer and explanation

AnswerError

The inner call half(10) returns 5. The outer call receives 5, which is odd, so the precondition fails and the program stops with a runtime error. Nothing is printed: print never receives a value.

Tests as examples

CS61A writes examples as doctests. In Swift, assert lines play the same role in a lab file: assert(choose(5, 2) == 10) does nothing when the answer is right and stops the program when it is wrong. In Unit 3 we will move to Swift’s testing library.

Functions that wrap functions

A higher-order function can add behavior to any function without changing it. trace prints each argument before calling the function it wraps, which is handy when debugging:

func trace(_ f: @escaping (Int) -> Int) -> (Int) -> Int {
    return { x in
        print("->", x)
        return f(x)
    }
}

let tracedSquare = trace { $0 * $0 }
print(tracedSquare(3))

What would Swift print?

func trace(_ f: @escaping (Int) -> Int) -> (Int) -> Int {
    return { x in
        print("->", x)
        return f(x)
    }
}
let t = trace { $0 + 1 }
print(t(t(1)))

Type Error if running it would crash. For several lines of output, put each on its own line.

Show answer and explanation

Answer-> 1\n-> 2\n3

The inner call t(1) runs first: it prints -> 1 and returns 2. Then t(2) prints -> 2 and returns 3, which the outer print writes.

Searching

search finds the smallest non-negative integer for which a condition holds:

func search(_ condition: (Int) -> Bool) -> Int {
    var x = 0
    while !condition(x) {
        x += 1
    }
    return x
}

print(search { $0 * $0 > 30 })   // 6

What would Swift print?

func search(_ condition: (Int) -> Bool) -> Int {
    var x = 0
    while !condition(x) {
        x += 1
    }
    return x
}
print(search { $0 % 7 == 3 && $0 > 10 })

Type Error if running it would crash. For several lines of output, put each on its own line.

Show answer and explanation

Answer17

The condition asks for a number above 10 that leaves remainder 3 when divided by 7. The candidates with remainder 3 are 3, 10, 17, …; the first one above 10 is 17.

Lab 5: Abstraction

Q1: Inverse

Using search, write inverse(f): given a function f that is increasing on the non-negative integers, return a function g such that g(f(x)) == x.

func inverse(_ f: @escaping (Int) -> Int) -> (Int) -> Int {
    // your code here
}
Show solution
func search(_ condition: (Int) -> Bool) -> Int {
    var x = 0
    while !condition(x) { x += 1 }
    return x
}

func inverse(_ f: @escaping (Int) -> Int) -> (Int) -> Int {
    return { y in search { x in f(x) == y } }
}

let squareRoot = inverse { $0 * $0 }
assert(squareRoot(16) == 4)
assert(squareRoot(144) == 12)
let halve = inverse { $0 * 2 }
assert(halve(10) == 5)

The inverse of f at y is “the x whose f(x) is y”, which is exactly a search. If y is not a value f ever takes, the search never stops, which is why the problem promises y is in range.

Q2: Count the calls

Write countCalls(f), which returns two functions in a tuple: call, which behaves exactly like f, and count, which returns how many times call has been called.

func countCalls(_ f: @escaping (Int) -> Int) -> (call: (Int) -> Int, count: () -> Int) {
    // your code here
}
Show solution
func countCalls(_ f: @escaping (Int) -> Int) -> (call: (Int) -> Int, count: () -> Int) {
    var calls = 0
    let call = { (x: Int) -> Int in
        calls += 1
        return f(x)
    }
    return (call, { calls })
}

let counted = countCalls { $0 * 10 }
assert(counted.count() == 0)
assert(counted.call(2) == 20)
assert(counted.call(3) == 30)
assert(counted.count() == 2)

Both closures capture the same calls variable from the frame of this call to countCalls, so count sees the updates that call makes.

Q3: Cube roots

Using improve from this lesson, write cubeRoot(_:) with Newton’s update for cube roots: a better guess for ∛a is (2x + a / x²) / 3. Stop when |x³ − a| < 10⁻¹⁰.

Show solution
func improve(_ update: (Double) -> Double, _ close: (Double) -> Bool, _ guess: Double = 1) -> Double {
    var guess = guess
    while !close(guess) { guess = update(guess) }
    return guess
}

func cubeRoot(_ a: Double) -> Double {
    improve({ x in (2 * x + a / (x * x)) / 3 }, { x in abs(x * x * x - a) < 1e-10 })
}

assert(abs(cubeRoot(27) - 3) < 1e-9)
assert(abs(cubeRoot(2) * cubeRoot(2) * cubeRoot(2) - 2) < 1e-9)

Only the two small closures change; the loop, the stopping logic, and the default starting guess all come from improve.

What’s next

Next lesson: recursion, where a function’s body calls the function itself.