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

Protocols and generics

Interfaces in Swift: protocols that name what a type can do, default implementations in extensions, the standard protocols behind printing and comparing, and generic functions and types.

CS61A
Object Examples (interfaces) · Composing Programs 2.7

CS61A describes an interface as a set of shared messages: if several kinds of object all respond to area, code can ask any of them for its area without knowing which kind it has. Python relies on convention for this. Swift writes the interface down as a protocol, and the compiler checks that every type claiming to follow it really does.

Protocols

A protocol lists requirements: properties and methods a conforming type must provide.

protocol Shape {
    var name: String { get }
    var area: Double { get }
}

struct Square: Shape {
    var side: Double
    var name: String { "square" }
    var area: Double { side * side }
}

struct Circle: Shape {
    var radius: Double
    var name: String { "circle" }
    var area: Double { Double.pi * radius * radius }
}

let shapes: [any Shape] = [Square(side: 2), Circle(radius: 1)]
for shape in shapes {
    print(shape.name, shape.area)
}

{ get } means the property must be readable; a stored or computed property both satisfy it. [any Shape] is an array whose elements can be any type that conforms to Shape. If Circle forgot area, the program would not compile.

Default implementations

An extension adds methods to an existing type, including a protocol. Methods added to a protocol extension are available to every conforming type:

protocol Animal {
    var name: String { get }
    func sound() -> String
}

extension Animal {
    func speak() -> String {
        "\(name) says \(sound())"
    }
}

struct Dog: Animal {
    let name: String
    func sound() -> String { "woof" }
}

print(Dog(name: "Rex").speak())   // Rex says woof

Dog only had to supply name and sound; speak came for free. This is how Swift shares behavior without inheritance, and it works for structures and enumerations as well as classes.

What would Swift print?

protocol Animal {
    var name: String { get }
    func sound() -> String
}
extension Animal {
    func speak() -> String { "\(name) says \(sound())" }
}
struct Cat: Animal {
    let name: String
    func sound() -> String { "meow" }
}
struct Cow: Animal {
    let name: String
    func sound() -> String { "moo" }
}
let animals: [any Animal] = [Cat(name: "Tom"), Cow(name: "Bess")]
print(animals.map { $0.speak() })

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

Show answer and explanation

Answer["Tom says meow", "Bess says moo"]

Each element uses its own name and sound, and the shared speak from the extension puts them together. map collects the two strings into an array.

Extensions work on types you did not write, too:

extension Int {
    var isEven: Bool { self % 2 == 0 }
}
print(4.isEven, 7.isEven)   // true false

The standard protocols

Much of what you have used so far is protocols from the standard library.

  • CustomStringConvertible: a description property that print uses.
  • Equatable: ==. For a structure whose properties are all equatable, Swift writes == for you.
  • Comparable: <, which also gives sorted(), max(), and friends.
  • Hashable: needed to be a dictionary key or set element; also synthesized for simple structures.
struct Money: CustomStringConvertible, Comparable, Hashable {
    let cents: Int

    var description: String {
        let remainder = cents % 100
        return "$\(cents / 100)." + (remainder < 10 ? "0" : "") + String(remainder)
    }

    static func < (a: Money, b: Money) -> Bool {
        a.cents < b.cents
    }
}

print(Money(cents: 1234))                                // $12.34
print([Money(cents: 300), Money(cents: 100)].sorted())   // [$1.00, $3.00]
print(Money(cents: 5) == Money(cents: 5))                // true

static func < defines the operator for this type; == and hashing were generated because cents is an Int.

What would Swift print?

struct Point: Equatable {
    var x: Int
    var y: Int
}
let a = Point(x: 1, y: 2)
print(a == Point(x: 1, y: 2), a == Point(x: 2, y: 1))

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

Show answer and explanation

Answertrue false

Declaring Equatable is enough: Swift compares the properties one by one. The second point has the same numbers in a different order, so it is not equal.

Generics

A generic function works for any type that meets some requirement, written as a type parameter in angle brackets:

func largest<T: Comparable>(_ values: [T]) -> T? {
    guard var best = values.first else {
        return nil
    }
    for value in values where value > best {
        best = value
    }
    return best
}

print(largest([3, 9, 2]) ?? 0)            // 9
print(largest(["pear", "apple"]) ?? "")   // pear

T: Comparable says: T can be any type, as long as it conforms to Comparable, which is exactly what > needs. One definition serves integers, strings, and Money, and Swift still checks types: a call on an array of shapes would not compile, because shapes cannot be compared.

Types can be generic too. Array<Int> is the full name of [Int]. Here is a stack that holds any element type:

struct Stack<Element> {
    private var items: [Element] = []

    var isEmpty: Bool { items.isEmpty }

    mutating func push(_ item: Element) {
        items.append(item)
    }

    mutating func pop() -> Element? {
        items.popLast()
    }
}

var stack = Stack<String>()
stack.push("a")
stack.push("b")
print(stack.pop()!, stack.pop()!, stack.isEmpty)   // b a true

Put the lines in order

Put the lines in order so the program prints 2 3.

  1. print(count([1, 2, 3, 4], where: { $0 > 2 }), count("banana", where: { $0 == "a" }))
  2. func count<S: Sequence>(_ items: S, where test: (S.Element) -> Bool) -> Int {
  3. total += 1
  4. var total = 0
  5. return total
  6. }
  7. for item in items where test(item) {
  8. }

Show the correct program
func count<S: Sequence>(_ items: S, where test: (S.Element) -> Bool) -> Int {
    var total = 0
    for item in items where test(item) {
        total += 1
    }
    return total
}
print(count([1, 2, 3, 4], where: { $0 > 2 }), count("banana", where: { $0 == "a" }))

Sequence is the protocol behind every for-in loop, and S.Element is whatever type it yields: Int for the array, Character for the string. Lesson 9’s “sequence abstraction” is this protocol.

Lab 15: Interfaces

Q1: A generic queue

Write Queue<Element>, a structure with enqueue(_:), dequeue() (returning an optional, first in first out), and a count property.

Show solution
struct Queue<Element> {
    private var items: [Element] = []

    var count: Int { items.count }

    mutating func enqueue(_ item: Element) {
        items.append(item)
    }

    mutating func dequeue() -> Element? {
        items.isEmpty ? nil : items.removeFirst()
    }
}

var queue = Queue<Int>()
queue.enqueue(1)
queue.enqueue(2)
assert(queue.count == 2)
assert(queue.dequeue() == 1)
assert(queue.dequeue() == 2)
assert(queue.dequeue() == nil)

removeFirst() shifts every remaining element, so this queue gets slow when large. Lesson 18 discusses measuring that; a faster queue keeps two stacks.

Q2: Describable shapes

Make Square and Circle from this lesson conform to CustomStringConvertible with a single extension on the Shape protocol, so that print(Square(side: 2)) prints square (area 4.0).

Show solution
protocol Shape: CustomStringConvertible {
    var name: String { get }
    var area: Double { get }
}

extension Shape {
    var description: String { "\(name) (area \(area))" }
}

struct Square: Shape {
    var side: Double
    var name: String { "square" }
    var area: Double { side * side }
}

print(Square(side: 2))
assert(Square(side: 2).description == "square (area 4.0)")

protocol Shape: CustomStringConvertible makes every shape describable, and the extension supplies description for all of them at once.

Q3: Smallest by a key

Write a generic function smallest(_:by:) that returns the element for which a key function gives the smallest value, or nil for an empty array. For example, the shortest word in an array of strings.

func smallest<T, Key: Comparable>(_ items: [T], by key: (T) -> Key) -> T? {
    // your code here
}
Show solution
func smallest<T, Key: Comparable>(_ items: [T], by key: (T) -> Key) -> T? {
    guard var best = items.first else {
        return nil
    }
    for item in items where key(item) < key(best) {
        best = item
    }
    return best
}

assert(smallest(["pear", "fig", "banana"], by: { $0.count }) == "fig")
assert(smallest([3, -7, 5], by: { abs($0) }) == 3)
assert(smallest([String](), by: { $0.count }) == nil)

Two type parameters: T for the elements, which need nothing, and Key for the key’s type, which must be comparable. The standard library’s version is items.min(by:).

What’s next

Next lesson: iterators and lazy sequences, and sequences that never end.