Menu
Coddy logo textTech

Guarding State

Part of the Object Oriented Programming section of Coddy's Swift journey. Lesson 19 of 57.

An invariant is a rule that must always hold for a value, such as "a balance is never negative". A type protects its invariants by checking every change before applying it:

struct BankAccount {
    private(set) var balance = 0

    mutating func withdraw(_ amount: Int) -> Bool {
        guard amount > 0, amount <= balance else { return false }
        balance -= amount
        return true
    }

    mutating func deposit(_ amount: Int) {
        guard amount > 0 else { return }
        balance += amount
    }
}

var acc = BankAccount()
acc.deposit(100)
print(acc.withdraw(150))
print(acc.withdraw(40))
print(acc.balance)

Output:

false
true
60

When a caller needs to know why a change was refused, the method can throw an error instead of returning false. The error type is an enum that conforms to Error:

enum TransferError: Error {
    case invalidAmount
    case insufficientFunds(missing: Int)
}

struct Account {
    private(set) var balance: Int

    mutating func withdraw(_ amount: Int) throws {
        guard amount > 0 else { throw TransferError.invalidAmount }
        guard amount <= balance else { throw TransferError.insufficientFunds(missing: amount - balance) }
        balance -= amount
    }
}

var acc = Account(balance: 30)
do {
    try acc.withdraw(50)
} catch TransferError.insufficientFunds(let missing) {
    print("missing \(missing)")
} catch {
    print("other error")
}

Output:

missing 20

Keep every change of the protected data inside methods that check it. If the property were settable from outside, any line of code could break the rule without the type noticing:

struct Temperature {
    private(set) var kelvin: Double   // must stay >= 0
    mutating func cool(by amount: Double) {
        kelvin = max(0, kelvin - amount)   // the only way to lower it
    }
}

An operation that changes several properties must leave them consistent even when it fails halfway. Check everything first, then change the state:

struct Inventory {
    private(set) var stock: [String: Int] = ["tea": 2, "jam": 1]

    mutating func sell(_ items: [String]) -> Bool {
        var needed: [String: Int] = [:]
        for i in items { needed[i, default: 0] += 1 }
        for (item, n) in needed where (stock[item] ?? 0) < n { return false }
        for (item, n) in needed { stock[item]! -= n }
        return true
    }
}

var inv = Inventory()
print(inv.sell(["tea", "jam", "jam"]))
print(inv.stock["tea"]!)
print(inv.sell(["tea", "jam"]))
print(inv.stock["tea"]!)

Output:

false
2
true
1
challenge icon

Challenge

Easy

Complete the struct Seats, which sells tickets for a show with capacity seats. sold is private(set). buy(_:) throws TicketError.invalidCount for a count below 1, TicketError.soldOut when no seat is left, and TicketError.notEnough(left:) when fewer seats are left than requested; otherwise it adds the tickets to sold.

The supplied code reads the capacity and one requested count per line. For each it prints sold 3, left 7, invalid count, sold out or only 2 left.

Your code goes in TicketError.swift and Seats.swift. main.swift holds the supplied input/output code and cannot be edited.

Try it yourself

// Supplied input/output code: keep it as it is
var input: [String] = []
while let line = readLine() { input.append(line) }
var seats = Seats(capacity: Int(input[0])!)
for line in input.dropFirst() {
    let n = Int(line)!
    do {
        try seats.buy(n)
        print("sold \(n), left \(seats.capacity - seats.sold)")
    } catch TicketError.invalidCount {
        print("invalid count")
    } catch TicketError.soldOut {
        print("sold out")
    } catch TicketError.notEnough(let left) {
        print("only \(left) left")
    } catch {
        print("unexpected")
    }
}
quiz iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Object Oriented Programming

Practice on your own: Swift playground