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

Linked lists and trees

Recursive data: a linked list as an indirect enum, a tree as a structure that holds more trees, and recursive functions whose shape follows the data.

CS61A
Linked Lists · Trees · Composing Programs 2.9
Swift book
Enumerations

Recursive functions call themselves. Recursive data contains itself: a list whose rest is a smaller list, a tree whose branches are smaller trees. When the data is recursive, the functions that process it almost write themselves, because their structure follows the data’s.

Linked lists

A linked list is either empty, or a first element followed by the rest of the list, which is itself a linked list. That sentence is the type definition:

indirect enum List {
    case empty
    case node(Int, List)
}

let numbers = List.node(1, .node(2, .node(3, .empty)))

indirect tells Swift that a case contains a value of the enum’s own type, so it must store that part by reference. Without it, a List would have to contain a whole List inside itself, forever, and the compiler rejects the definition: “recursive enum ‘List’ is not marked ‘indirect’”.

Functions that follow the data

Every function over a list handles the two cases, and the node case recurses on the rest:

indirect enum List {
    case empty
    case node(Int, List)
}

func length(_ list: List) -> Int {
    switch list {
    case .empty: 0
    case .node(_, let rest): 1 + length(rest)
    }
}

func sum(_ list: List) -> Int {
    switch list {
    case .empty: 0
    case .node(let first, let rest): first + sum(rest)
    }
}

let numbers = List.node(1, .node(2, .node(3, .empty)))
print(length(numbers), sum(numbers))   // 3 6

The recursive leap of faith from Lesson 7 applies unchanged: assume the function works on rest, and combine that with first.

What would Swift print?

indirect enum List {
    case empty
    case node(Int, List)
}
func toArray(_ list: List) -> [Int] {
    switch list {
    case .empty: []
    case .node(let first, let rest): [first] + toArray(rest)
    }
}
func map(_ list: List, _ f: (Int) -> Int) -> List {
    switch list {
    case .empty: .empty
    case .node(let first, let rest): .node(f(first), map(rest, f))
    }
}
let numbers = List.node(1, .node(2, .node(3, .empty)))
print(toArray(map(numbers) { $0 * 10 }))

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

Show answer and explanation

Answer[10, 20, 30]

map builds a new list with f applied to each element, keeping the same shape. toArray then collects the elements in order.

Linked lists make adding to the front cheap: .node(0, numbers) is a new list that shares all of numbers without copying it. Arrays are better for most everyday work; linked lists are here because they are the simplest recursive data, and the pattern carries over to trees.

Trees

A tree has a label and a list of branches, each of which is a tree. A tree with no branches is a leaf. In Swift a structure can hold an array of its own type directly:

struct Tree {
    var label: Int
    var branches: [Tree] = []

    var isLeaf: Bool { branches.isEmpty }
}

//        1
//       / \
//      2   3
//      |
//      4
let t = Tree(label: 1, branches: [
    Tree(label: 2, branches: [Tree(label: 4)]),
    Tree(label: 3),
])

branches: [Tree] = [] gives the property a default, so a leaf can be made with just Tree(label: 4).

Tree recursion over trees

A function over a tree handles the leaf, and otherwise combines the results of calling itself on every branch. map and reduce from Lesson 4 do the combining:

struct Tree {
    var label: Int
    var branches: [Tree] = []
    var isLeaf: Bool { branches.isEmpty }
}

func countLeaves(_ t: Tree) -> Int {
    t.isLeaf ? 1 : t.branches.map(countLeaves).reduce(0, +)
}

func height(_ t: Tree) -> Int {
    t.isLeaf ? 0 : 1 + t.branches.map(height).max()!
}

func sumLabels(_ t: Tree) -> Int {
    t.label + t.branches.map(sumLabels).reduce(0, +)
}

let t = Tree(label: 1, branches: [Tree(label: 2, branches: [Tree(label: 4)]), Tree(label: 3)])
print(countLeaves(t), height(t), sumLabels(t))   // 2 2 10

Passing countLeaves to map inside countLeaves looks strange at first. It is the same leap of faith: to count the leaves of this tree, count the leaves of each branch and add.

What would Swift print?

struct Tree {
    var label: Int
    var branches: [Tree] = []
}
func printTree(_ t: Tree, _ depth: Int = 0) {
    print(String(repeating: "-", count: depth) + String(t.label))
    for branch in t.branches {
        printTree(branch, depth + 1)
    }
}
printTree(Tree(label: 1, branches: [Tree(label: 2, branches: [Tree(label: 4)]), Tree(label: 3)]))

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

Show answer and explanation

Answer1\n-2\n--4\n-3

printTree prints a label, then each branch one level deeper. The whole branch under 2, including 4, is printed before moving on to 3: the calls go down one branch completely before the next, just like cascade in Lesson 7.

Building trees

Tree-recursive computations produce trees naturally. The tree of calls from Lesson 8’s fib can be built as data, with each label equal to that Fibonacci number:

struct Tree {
    var label: Int
    var branches: [Tree] = []
}

func fibTree(_ n: Int) -> Tree {
    if n <= 1 {
        return Tree(label: n)
    }
    let left = fibTree(n - 2)
    let right = fibTree(n - 1)
    return Tree(label: left.label + right.label, branches: [left, right])
}

print(fibTree(5).label)   // 5

Put the lines in order

Put the lines in order: leaves(t) returns the labels of the leaves from left to right, and the program prints [4, 3].

  1. var branches: [Tree] = []
  2. if t.branches.isEmpty {
  3. }
  4. var label: Int
  5. print(leaves(Tree(label: 1, branches: [Tree(label: 2, branches: [Tree(label: 4)]), Tree(label: 3)])))
  6. return t.branches.flatMap(leaves)
  7. return [t.label]
  8. struct Tree {
  9. }
  10. func leaves(_ t: Tree) -> [Int] {
  11. }

Show the correct program
struct Tree {
    var label: Int
    var branches: [Tree] = []
}
func leaves(_ t: Tree) -> [Int] {
    if t.branches.isEmpty {
        return [t.label]
    }
    return t.branches.flatMap(leaves)
}
print(leaves(Tree(label: 1, branches: [Tree(label: 2, branches: [Tree(label: 4)]), Tree(label: 3)])))

flatMap is map followed by joining the resulting arrays into one.

Lab 12: Recursive data

Q1: Build a list

Write fromArray(_:), which builds a List from an array, and toArray(_:), which does the reverse. toArray(fromArray(a)) should be a.

Show solution
indirect enum List {
    case empty
    case node(Int, List)
}

func fromArray(_ values: [Int]) -> List {
    guard let first = values.first else {
        return .empty
    }
    return .node(first, fromArray(Array(values.dropFirst())))
}

func toArray(_ list: List) -> [Int] {
    switch list {
    case .empty: []
    case .node(let first, let rest): [first] + toArray(rest)
    }
}

assert(toArray(fromArray([4, 5, 6])) == [4, 5, 6])
assert(toArray(fromArray([])) == [])

fromArray recurses on the array without its first element; toArray recurses on the list’s rest. The two definitions mirror each other.

Q2: Maximum path sum

Write maxPathSum(_:), the largest sum of labels along any path from the root down to a leaf.

func maxPathSum(_ t: Tree) -> Int {
    // your code here
}
Show solution
struct Tree {
    var label: Int
    var branches: [Tree] = []
}

func maxPathSum(_ t: Tree) -> Int {
    t.label + (t.branches.map(maxPathSum).max() ?? 0)
}

let t = Tree(label: 1, branches: [Tree(label: 2, branches: [Tree(label: 4)]), Tree(label: 3)])
assert(maxPathSum(t) == 7)              // 1 + 2 + 4
assert(maxPathSum(Tree(label: 5)) == 5)

max() of an empty array is nil, so ?? 0 covers leaves without a separate case.

Q3: Has a path

Write hasPath(_:_:), which returns true if the array of labels appears as a path starting at the root and going down through branches.

func hasPath(_ t: Tree, _ labels: [Int]) -> Bool {
    // your code here
}
Show solution
struct Tree {
    var label: Int
    var branches: [Tree] = []
}

func hasPath(_ t: Tree, _ labels: [Int]) -> Bool {
    guard let first = labels.first, first == t.label else {
        return false
    }
    if labels.count == 1 {
        return true
    }
    let rest = Array(labels.dropFirst())
    return t.branches.contains { hasPath($0, rest) }
}

let t = Tree(label: 1, branches: [Tree(label: 2, branches: [Tree(label: 4)]), Tree(label: 3)])
assert(hasPath(t, [1, 2, 4]))
assert(hasPath(t, [1, 3]))
assert(!hasPath(t, [1, 4]))
assert(!hasPath(t, [2]))
assert(!hasPath(t, []))

contains(where:), written here with a trailing closure, is true if any branch has the rest of the path. It stops at the first branch that does.

Q4: Replace a leaf

Write replaceLeaf(_:_:_:), which returns a new tree with every leaf labeled old relabeled new. Labels that are not leaves stay the same.

Show solution
struct Tree {
    var label: Int
    var branches: [Tree] = []
    var isLeaf: Bool { branches.isEmpty }
}

func leaves(_ t: Tree) -> [Int] {
    t.isLeaf ? [t.label] : t.branches.flatMap(leaves)
}

func replaceLeaf(_ t: Tree, _ old: Int, _ new: Int) -> Tree {
    if t.isLeaf {
        return Tree(label: t.label == old ? new : t.label)
    }
    return Tree(label: t.label, branches: t.branches.map { replaceLeaf($0, old, new) })
}

let t = Tree(label: 4, branches: [Tree(label: 2, branches: [Tree(label: 4)]), Tree(label: 3)])
let replaced = replaceLeaf(t, 4, 40)
assert(leaves(replaced) == [40, 3])
assert(replaced.label == 4)       // the root is not a leaf
assert(leaves(t) == [4, 3])       // the original is unchanged

The function builds a new tree instead of changing the old one. Because Tree is a structure, the original could not be changed through t anyway, which is next lesson’s topic.

What’s next

Next lesson: mutation, and the difference between changing a value and changing a shared object.