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

Environments and closures

Frames, name lookup, nested functions, and closures that capture the variables around them, drawn as CS61A-style environment diagrams.

CS61A
Environments · Composing Programs 1.6
Swift book
Closures

Last lesson, makeAdder(3) returned a closure that still knew n was 3 long after makeAdder had finished. To explain why, CS61A uses environment diagrams: a picture of which names are bound to which values, and where Swift looks when it meets a name.

Frames and environments

A frame is a table of bindings from names to values. The global frame holds everything bound at the top level of the program. Each call to a function creates a new local frame for its parameters and local names.

An environment is a sequence of frames. To look up a name, Swift checks the innermost frame first, then the frame it was defined in, and so on out to the global frame. The first binding found wins.

let x = 10

func f() -> Int {
    let x = 20
    return x
}

print(f(), x)   // 20 10

Here is the diagram at the moment f returns:

Global frame
  x │ 10
  f │ func f()  [parent = Global]

f  [parent = Global]
  x      │ 20
  Return │ 20

Inside f, the local x shadows the global one: lookup stops at the first frame that has the name. The global x is never changed.

What would Swift print?

let n = 1
func g(_ n: Int) -> Int {
    return n * 10
}
print(g(5), n)

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

Show answer and explanation

Answer50 1

The call g(5) creates a frame where the parameter n is 5, which shadows the global n. The body returns 50. The global n is still 1.

Nested functions

A function can be defined inside another function. The inner function’s parent frame is the frame of the call that defined it, so it can use the outer function’s names:

func sumOfShifts(_ n: Int) -> Int {
    func shifted(_ k: Int) -> Int {
        k + n       // n comes from sumOfShifts's frame
    }
    return shifted(1) + shifted(2)
}
print(sumOfShifts(10))   // 23

This is lexical scoping: what a name means depends on where the code is written, not on who calls it.

Closures capture

When a closure is returned from a function, the frame it was defined in does not disappear. The closure captures the names it uses and keeps them alive:

func makeCounter() -> () -> Int {
    var count = 0
    return {
        count += 1
        return count
    }
}

let a = makeCounter()
print(a(), a(), a())   // 1 2 3

In diagram form, after the first call to a:

Global frame
  makeCounter │ func makeCounter()  [parent = Global]
  a           │ closure  [parent = f1]

f1: makeCounter  [parent = Global]
  count  │ 1          ← kept alive because the closure captured it
  Return │ closure

Each call to makeCounter makes a new frame, so each counter gets its own count.

What would Swift print?

func makeCounter() -> () -> Int {
    var count = 0
    return {
        count += 1
        return count
    }
}
let a = makeCounter()
let b = makeCounter()
print(a(), a(), b())

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

Show answer and explanation

Answer1 2 1

a and b come from two different calls, so they capture two different count variables. a counts to 2 while b starts again at 1.

Closures are reference types. Binding a closure to a second name does not copy its captured state; both names refer to the same closure.

What would Swift print?

func makeCounter() -> () -> Int {
    var count = 0
    return {
        count += 1
        return count
    }
}
let a = makeCounter()
_ = a()
let alsoA = a
print(alsoA(), a())

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

Show answer and explanation

Answer2 3

a has already counted to 1. alsoA is the same closure, so calling it gives 2, and calling a afterward gives 3.

Capturing a variable, not a value

A closure captures the variable itself, so it sees later changes. To capture the value at the moment the closure is created, list the name in a capture list in square brackets:

What would Swift print?

var x = 1
let live = { print("live", x) }
let frozen = { [x] in print("frozen", x) }
x = 2
live()
frozen()

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

Show answer and explanation

Answerlive 2\nfrozen 1

live reads x when it runs, after x has become 2. frozen copied the value of x into its capture list when it was created, when x was still 1.

Put the lines in order

Put the lines in order so the program prints 5 15 12.

  1. func makeAccumulator() -> (Int) -> Int {
  2. var total = 0
  3. let deposit = makeAccumulator()
  4. return { amount in
  5. }
  6. total += amount
  7. print(deposit(5), deposit(10), deposit(-3))
  8. return total
  9. }

Show the correct program
func makeAccumulator() -> (Int) -> Int {
    var total = 0
    return { amount in
        total += amount
        return total
    }
}
let deposit = makeAccumulator()
print(deposit(5), deposit(10), deposit(-3))

Lab 4: Environments

Q1: Draw it

Without running the code, draw the environment diagram at the moment the program prints, and say what it prints.

func makeMultiplier(_ factor: Int) -> (Int) -> Int {
    return { x in x * factor }
}
let factor = 100
let triple = makeMultiplier(3)
print(triple(factor))
Show solution

It prints 300.

Global frame
  makeMultiplier │ func makeMultiplier(_:)  [parent = Global]
  factor         │ 100
  triple         │ closure  [parent = f1]

f1: makeMultiplier  [parent = Global]
  factor │ 3
  Return │ closure

f2: closure  [parent = f1]
  x      │ 100
  Return │ 300

The argument factor in triple(factor) is looked up in the global frame, giving 100. Inside the closure (frame f2), factor is looked up starting from the closure’s parent, frame f1, where it is 3. The same name means two different things in two places.

Q2: Curry

curry2 turns a two-argument function into a function that takes one argument and returns another one-argument function: curry2(f)(x)(y) is f(x, y).

func curry2(_ f: @escaping (Int, Int) -> Int) -> (Int) -> (Int) -> Int {
    // your code here
}
Show solution
func curry2(_ f: @escaping (Int, Int) -> Int) -> (Int) -> (Int) -> Int {
    return { x in
        return { y in f(x, y) }
    }
}

let add = curry2(+)
let addFive = add(5)
assert(addFive(3) == 8)
assert(curry2(*)(4)(5) == 20)
assert(curry2(-)(10)(3) == 7)

The inner closure uses both x from its parent closure’s frame and f from curry2’s frame. Each layer of closure adds one frame to the chain.

Q3: A Fibonacci generator

Write makeFibber(), which returns a function that returns the next Fibonacci number each time it is called: 0, 1, 1, 2, 3, 5, …

func makeFibber() -> () -> Int {
    // your code here
}
Show solution
func makeFibber() -> () -> Int {
    var current = 0
    var next = 1
    return {
        let result = current
        (current, next) = (next, current + next)
        return result
    }
}

let fibber = makeFibber()
let firstSix = (1...6).map { _ in fibber() }
assert(firstSix == [0, 1, 1, 2, 3, 5])

let another = makeFibber()
assert(another() == 0)   // a separate frame, starting over
assert(fibber() == 8)    // the first fibber continues

current and next live in the frame of the makeFibber call and are captured by the returned closure. They are the state that survives between calls.

What’s next

Next lesson: using everything so far to design functions well, with generalization, documentation, and tests.