Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Preface

This book teaches Swift to someone who already programs. If you have written loops and functions in Lux, Python, JavaScript, or anything of that family, you have what you need. You will not be told what a variable is. You will be told what Swift does with one, which is a different and more interesting question.

Three things will be new. The first is that types are checked before the program runs rather than while it runs, so a class of mistake that a scripting language reports at three in the morning is reported by the compiler at your desk. The second is that Swift has no null: a value that might be missing is wrapped in an Optional and the compiler will not let you forget to open it, which is Chapter 5. The third is the one that takes longest to feel natural. Swift asks you to decide whether a value is copied when you pass it around or shared, you make that decision every time you define a type, and it changes what your program means. Chapter 7 is where that gets its due.

The pace is brisk on purpose. Programs come first and explanation follows, and the explanation stops when the point has been made. A feature you can already guess the shape of gets a sentence. A feature that will bite you gets a page. If you want the complete description of some corner of the language, The Swift Programming Language is free online and this book is not trying to replace it. What this book is trying to do is get you writing real Swift in a week rather than a month.

Read it with a keyboard. Type the programs in, run them, then break them and read what the compiler says. The exercises are short and most of them are variations on the program just presented; do the ones that look interesting and skip the rest without guilt. Each has a worked answer in examples/solutions, which is a worse place to start than your own keyboard.

Getting the compiler

Install Swift from https://www.swift.org/install/. There are packages for macOS, Linux, and Windows, and swiftly will install and manage toolchains on macOS and Linux the way rustup does for Rust. You do not need Xcode and none of the programs here use an Apple framework. Check the install with

swift --version

Everything in this book is Swift 6, compiled in language mode 6, which is what you get by default from a Swift 6 toolchain in a package and what the -swift-version 6 flag asks for when compiling a single file. Strict concurrency checking is part of that mode and it stays quiet until Chapter 12, where it becomes the point.

About the examples

Every complete program in this book is a real file in the examples directory beside the text, and the text quotes the file rather than a tidied-up version of it. Compile one on its own:

swiftc -swift-version 6 examples/ch01/fahr2.swift -o fahr2
./fahr2

On Linux, a single file that calls a floating-point function such as rounded() may fail to link with undefined reference to symbol 'round'. Add -lm, which is harmless everywhere else, or import Foundation, which pulls it in as a side effect. The checker described below compiles with -lm for that reason.

Chapter 11 builds a program properly, with a package, modules and tests, and from there on you use swift build and swift run instead.

Every output printed in this book came from running the program, and every compiler message quoted came from making the mistake. tools/check.sh compiles each program, runs the ones that have recorded output, checks that output still matches, and confirms that the code printed in the text is identical to the code on disk. If something here does not behave the way the text claims, the text is wrong and you should trust the compiler.

The programs that deliberately fail to compile live in examples/errors, one per mistake, and Appendix C collects their messages.

A note if you are coming from Lux

Lux already taught you a good deal of Swift’s data model without naming it. Values are immutable unless you say otherwise, there is no null, a function that might fail hands back an Option or a Result, and match has to cover every case. All four of those are Swift, spelled let, Optional, throws, and switch, and they are the parts of Swift that people arriving from Python find strangest. You are further along than you think.

You also have a shortcut nobody else gets. lux convert swift translates a Lux program into Swift source, so a program you already understand can be read back to you in the new syntax. Do that with something you wrote and you will recognize most of this book before you start it. Appendix B maps the two languages construct by construct, along with Python and JavaScript.

1. A Tutorial Introduction

This chapter is a tour. It moves fast, shows real programs, and leaves the careful definitions for later chapters. The goal is to get enough Swift into your hands that the rest of the book has something to build on. Things you do not fully understand here are explained properly further along, and the text says where.

1.1 Getting Started

The only way to start.

print("hello, world")

Put it in a file called hello.swift, compile it with swiftc hello.swift, and run the hello that appears beside it.

There is no main function and no enclosing class. Statements written at the top level of a file run in order, which makes a Swift file about as ceremonious as a Python one. A larger program does eventually want a proper entry point, and Chapter 11 shows how, but nothing before it needs one.

print is a function, not a keyword and not a macro. It takes any number of values, prints them separated by spaces, and adds a newline. The separator and the ending are arguments you can change, which is how you print without the newline:

print("no", "newline", separator: "-", terminator: "")

Exercise 1-1. Run the program. Then delete the closing parenthesis and compile again. Read the error.

Exercise 1-2. Print several lines with one print call. Find out what \n and \t do inside the quotes, and what \\ and \" are for.

1.2 Variables and Arithmetic Expressions

The next program prints a table of Fahrenheit temperatures and their Celsius equivalents, using the formula C = (5/9)(F−32).

// Print a Fahrenheit-Celsius table for fahr = 0, 20, ..., 300.
let lower = 0
let upper = 300
let step = 20

var fahr = lower
while fahr <= upper {
    let celsius = 5 * (fahr - 32) / 9
    print("\(fahr)\t\(celsius)")
    fahr += step
}

It prints:

0	-17
20	-6
40	4
...
300	148

Comments run from // to the end of the line. /* ... */ also works and nests properly, but the line form is what Swift programmers use.

let introduces a name whose value never changes; var introduces one that can. Immutable is the default and mutable is the word you type, which is backwards from most scripting languages and is the first small example of Swift’s general habit: the restricted thing is free and the flexible thing costs a word. fahr is a var because the loop assigns to it. The other three never change, so they are not. If you get it wrong the compiler is specific about it.

assign_let.swift:2:1: error: cannot assign to value: 'fahr' is a 'let' constant
1 | let fahr = 0
  | `- note: change 'let' to 'var' to make it mutable
2 | fahr = fahr + 20
  | `- error: cannot assign to value: 'fahr' is a 'let' constant

The note is usually the literal fix, and it repays reading rather than skimming. Appendix C takes the diagnostics apart properly, and is better read once you have collected a few of your own.

Notice there are no type declarations. Swift works out that these are integers from the values they are given and settles on Int, a signed integer the width of a machine word, which is 64 bits everywhere you are likely to run this. You can write the type when you want to, and sometimes you must:

let upper: Double = 300

The while loop is what you expect, except that the condition needs no parentheses and the braces are not optional, not even around a single statement. fahr += step adds and assigns. There is no ++.

Inside a string, \(fahr) substitutes the value of the expression between the parentheses. That is string interpolation, it takes any expression and not just a name, and it is how nearly all Swift output gets built:

print("\(fahr) \(5 * (fahr - 32) / 9)")

Now the arithmetic, where this program lies to you. 5 * (fahr - 32) / 9 is integer arithmetic throughout, so the division truncates toward zero and the table is wrong in the last digit. C has the same trap and K&R spends the same paragraph on it. The fix is floating point.

// Same table, floating point, right-aligned columns.
import Foundation

let lower = 0.0
let upper = 300.0
let step = 20.0

var fahr = lower
while fahr <= upper {
    let celsius = (fahr - 32.0) * 5.0 / 9.0
    print(String(format: "%3.0f %6.1f", fahr, celsius))
    fahr += step
}
  0  -17.8
 20   -6.7
 40    4.4
 60   15.6
 80   26.7
100   37.8

A literal with a decimal point is a Double, a 64-bit float. The standard library has no formatted printing, so column alignment comes from Foundation’s String(format:), which is C’s printf with the same conversions: %3.0f means a minimum width of three characters and no digits after the point, %6.1f means width six and one digit. Width pads, it does not truncate. import Foundation is the line that makes it available, and you will type it often, because Swift’s standard library is deliberately small and Foundation holds dates, files, regular expressions and the rest of what a program needs.

Swift does not convert between number types on its own. Adding an Int to a Double is not an error you have to think hard to make:

mixed_numbers.swift:3:17: error: binary operator '+' cannot be applied to operands of type 'Double' and 'Int'
3 | let next = fahr + step
  |                 |- error: binary operator '+' cannot be applied to operands of type 'Double' and 'Int'
  |                 `- note: overloads for '+' exist with these partially matching parameter lists: (Double, Double), (Int, Int)

Conversions are written by naming the type you want, Double(step) or Int(fahr). Literals are the exception and are looser than that suggests, so 5 * (fahr - 32.0) / 9 compiles even with fahr a Double. Chapter 2 says why, and what each conversion costs.

Exercise 1-3. Add a heading to the table.

Exercise 1-4. Print the table in reverse order, from 300 degrees to 0.

Exercise 1-5. Write a program that converts Celsius to Fahrenheit.

1.3 The for Statement

The same table again, written the way a Swift programmer would write it:

// Same table again, as a for loop over a range.
import Foundation

for i in 0...15 {
    let fahr = Double(i) * 20.0
    print(String(format: "%3.0f %6.1f", fahr, (fahr - 32.0) * 5.0 / 9.0))
}

0...15 is a range that includes both ends. 0..<16 is the same range with the top end excluded, and the half-open form is the one you will use most, because 0..<a.count is the natural way to talk about the positions in a collection of a.count things. Getting the two mixed up is the classic off-by-one, and Swift at least makes you say which you meant.

Swift’s for is not C’s. It has no three clauses; it walks something. A range is one such something, and so is an array, a dictionary, a string, or the lines of a file. This is the same for you know from Python, with the same comfortable property that the loop cannot run off the end of the thing it is walking.

Double(i) converts the integer to a float. It looks like a cast and is in fact a call to an initializer on the type, which is why a conversion behaves like the ordinary function call it is.

The loop variable is a let even though you did not write one, so assigning to i inside the loop is an error. Say for var i in ... if you need to, and you almost never do.

Exercise 1-6. Rewrite the temperature table as a loop over Fahrenheit values rather than a count, using stride(from: 0, through: 300, by: 20).

1.4 Input and Output

Here is a program that copies its input to its output.

// Copy input to output, a line at a time.
while let line = readLine(strippingNewline: false) {
    print(line, terminator: "")
}

Run it with ./copy < in.txt and it reproduces the file.

readLine() reads one line from standard input and hands back the text of it. When the input runs out there is no line to hand back, and this is the moment Swift differs from every scripting language you have used. It does not return an empty string, and it does not return null, because Swift has no null. It returns a String?, an optional string, which either contains a String or contains nothing, and which the compiler treats as a different type:

unwrapped_line.swift:2:7: error: value of optional type 'String?' must be unwrapped to refer to member 'count' of wrapped base type 'String'
1 | let line = readLine()
2 | print(line.count)
  |       |- note: chain the optional using '?' to access member 'count' only for non-'nil' base values
  |       `- note: force-unwrap using '!' to abort execution if the optional value contains 'nil'

You cannot use the string without saying what should happen if it is not there. while let line = readLine(...) is the short way of saying it: take the value out, bind it to line, run the body, and stop the loop the first time there is nothing to take. The loop condition and the unwrapping are one move. Chapter 5 is about optionals and they are the feature you will meet most often, so it comes early.

strippingNewline: false keeps the line ending, which a copy program wants. Without it you get the line without its newline, which is what everything else wants. That name before the colon is an argument label, and Section 1.6 explains why Swift calls arguments by name.

Counting

This is wc, more or less: lines, words, and bytes.

// Count lines, words, and bytes on standard input.
var text = ""
while let line = readLine(strippingNewline: false) {
    text += line
}

var lines = 0
var words = 0
var inWord = false

for c in text {
    if c == "\n" {
        lines += 1
    }
    if c == " " || c == "\n" || c == "\t" {
        inWord = false
    } else if !inWord {
        inWord = true
        words += 1
    }
}

print(lines, words, text.utf8.count)

On a two-line file it agrees with the system’s wc:

$ ./count < in.txt
2 6 28
$ wc < in.txt
 2  6 28

The word-counting logic is the state machine from K&R: you are in a word or you are not, and a transition from “not” to “in” is a new word.

Walking a string with for c in text gives you Character values, which are neither bytes nor Unicode code points. That is why the byte count here is text.utf8.count and not text.count: the two differ the moment anything outside English shows up, and Swift makes you name the one you meant. Chapter 6 is about the difference.

print(lines, words, text.utf8.count) passes three values and gets them separated by spaces, which is print’s default and is why the output lines up with wc without any formatting.

Arrays

Counting occurrences of each digit, plus white space and everything else:

// Count digits, white space, and everything else.
var text = ""
while let line = readLine(strippingNewline: false) {
    text += line
}

var digits = [Int](repeating: 0, count: 10)
var white = 0
var other = 0

for c in text {
    switch c {
    case "0"..."9":
        digits[c.wholeNumberValue!] += 1
    case " ", "\t", "\n":
        white += 1
    default:
        other += 1
    }
}

var line = "digits ="
for n in digits {
    line += " \(n)"
}
print("\(line), white space = \(white), other = \(other)")
$ printf 'abc 123 4\t56\n' | ./histogram
digits = 0 1 1 1 1 1 1 0 0 0, white space = 4, other = 3

[Int](repeating: 0, count: 10) makes an array of ten integers, all zero. A Swift array grows and shrinks, its length is not part of its type, and you get at the elements with digits[i] counting from zero. An index past the end stops the program with a message rather than reading whatever is next in memory.

switch is the other new thing, and it is the control structure Swift leans on hardest. Here "0"..."9" is a range pattern, " ", "\t", "\n" is a list of alternatives, and default catches the rest, which a Character needs because a switch has to cover every case. Chapter 3 is where it gets its due, and Chapter 8 shows why the language is built around it.

c.wholeNumberValue is the digit’s numeric value, and it is an optional, because most characters do not have one. The ! after it is a force unwrap: the short, rude way of saying “this cannot be nothing, and if it is, crash”. It is safe here, because the case above it has already established that c is a digit, and it is a smell almost everywhere else. Chapter 5 gives you the polite forms.

Exercise 1-7. Write a program that prints its input one word per line.

Exercise 1-8. Count blanks, tabs, and newlines separately.

Exercise 1-9. Copy input to output, replacing each run of one or more blanks with a single blank.

1.5 Functions

A function to raise an integer to a power:

// Raise base to the n-th power; n >= 0.
func power(_ base: Int, _ n: Int) -> Int {
    var p = 1
    for _ in 0..<n {
        p *= base
    }
    return p
}

for i in 0..<10 {
    print(i, power(2, i), power(-3, i))
}
0 1 1
1 2 -3
2 4 9
3 8 -27
...
9 512 -19683

func introduces a function. Parameters must have their types written out, and so must the return type, after the arrow. A function that returns nothing omits the arrow. Inside the body, inference takes over again: p is an Int because it starts at 1 and is multiplied by one.

return is required, with one exception: a function whose body is a single expression may leave it out, so func double(_ n: Int) -> Int { n * 2 } is complete. Swift programmers use that for one-liners and write return everywhere else.

The loop variable is _, which is how you say you do not intend to use it. The underscore means “discard this” throughout Swift, and you will see it in several places before the book is out.

Exercise 1-10. Write power using recursion.

Exercise 1-11. power(2, 63) stops the program rather than wrapping around to a negative number the way C would. Find out what &* does instead, and when you would want it.

1.6 Arguments

Swift calls arguments by name, and that is what the underscore in power’s definition was turning off. Python has keyword arguments too, but they are optional and Swift’s are not: the labels are part of the call and leaving one out is an error.

A parameter has two names, the label the caller writes and the name the body uses. Write one name and it serves as both.

func move(from: Int, to: Int) -> Int { to - from }
let d = move(from: 3, to: 10)

Write two and the first is the caller’s, the second the body’s, which is how you get a call that reads as a sentence and a body that reads as code:

func move(from start: Int, to end: Int) -> Int { end - start }

Write _ as the label and the caller writes nothing, which is what power did and what you want when the argument’s role is obvious from the function’s name. The labels are part of the function’s identity: move(from:to:) is its full name, and two functions may share the word move and differ only in their labels. Getting this right is most of what makes Swift code read the way it does, and Chapter 4 covers the rest of the rules.

Arguments arrive as constants. A parameter is a let inside the body, so power cannot count n down to zero the way the C version does and loops over a range instead. To hand a function something it can modify in place you mark the parameter inout and the caller writes & at the call, and Chapter 4 shows that.

What gets copied is the larger question. Passing an Int copies it and nothing is surprising. Passing an array or a string or a value of a type you defined with struct also copies it, so the function cannot change the caller’s copy however hard it tries. Passing an object of a type you defined with class does not copy: both sides hold the same object, and a change made through one is visible through the other. That difference is the subject of Chapter 7 and there is nothing to memorize until then.

1.7 Strings

A last program for this chapter: print the longest line of the input.

// Print the longest input line.
var longest = ""

while let line = readLine() {
    if line.count > longest.count {
        longest = line
    }
}

if !longest.isEmpty {
    print("\(longest.count) characters: \(longest)")
}
$ ./longest < in.txt
14 characters: this is a test

K&R’s version of this program takes three functions and a #define MAXLINE 1000, and truncates any line longer than that. This one has no maximum, because a Swift String grows to fit what you put in it and frees itself when the last name referring to it goes away. You never write the allocation and you never write the free.

longest = line looks like it copies the text and does not. Swift strings and arrays share their storage until somebody writes to one of them, at which point the writer gets its own copy. You get the behavior of copying, which is simple to reason about, and you pay for it only when a copy is actually needed. Chapter 7 is where that machinery gets explained; until then you can read every assignment as a copy and never be wrong about what the program does.

line.count counts characters, and Chapter 6 will point out that this walks the whole string rather than reading a stored length. On the lines of a text file nobody notices.

Exercise 1-12. Print all lines longer than 80 characters.

Exercise 1-13. Print each input line with trailing blanks and tabs removed, and skip lines that are empty afterwards.

Exercise 1-14. Print a histogram of the lengths of words in the input, one line per length, using * for the bars.

1.8 What You Have Seen

Enough Swift to write small programs: let and var and the preference for the first, the numeric types and the refusal to mix them silently, while and for over ranges, switch with its insistence on covering every case, functions with labeled arguments, and two things that will each get a chapter, in readLine handing back a value that might not be there and in the promise that assigning a string copies it.

What you have not seen is why any of it is arranged this way. The next chapters answer that, and Chapters 5 and 7 answer the largest part of it.

2. Types, Operators, and Expressions

Every value in Swift has a type, the type is known before the program runs, and nothing is converted behind your back. Those three facts account for most of what this chapter contains and for most of the errors you will see in your first week.

2.1 Names

A name is a letter or underscore followed by letters, digits and underscores. Swift’s idea of a letter is Unicode’s, so Δx and café are names, and so are most emoji, which is a joke the language allows and you should not accept. Swift’s convention is lowerCamelCase for variables, functions and properties, UpperCamelCase for types, and no underscores in either. Capitalization is how a reader tells a type from a value, so it is worth obeying.

Keywords like let, func and class are reserved, but only softly. Wrap a keyword in backticks and it becomes a name:

let `class` = "Physics 101"

That exists so Swift can talk to systems whose field names it does not control, and it is not for you.

Prefer let. The compiler warns when a local var is never assigned a second time, and says which word to change, so the habit gets enforced from the first day.

2.2 Integers

Int is the integer type. It is signed, it is the width of a pointer on the machine, and that means 64 bits everywhere you are likely to run. Use it. The sized types Int8, Int16, Int32, Int64 and their unsigned partners UInt8 through UInt64 exist for when the width is part of the problem, as in a file format or a hardware register, and UInt exists and is almost always the wrong choice. A quantity that cannot be negative is still an Int, because the moment you subtract two of them you want the answer to be allowed to go below zero.

// What the numeric types hold.
print("Int      ", Int.min, Int.max)
print("Int8     ", Int8.min, Int8.max)
print("UInt8    ", UInt8.min, UInt8.max)
print("Int32    ", Int32.min, Int32.max)
print("Double   ", Double.leastNormalMagnitude, Double.greatestFiniteMagnitude)
print("Int size ", MemoryLayout<Int>.size, "bytes")
Int       -9223372036854775808 9223372036854775807
Int8      -128 127
UInt8     0 255
Int32     -2147483648 2147483647
Double    2.2250738585072014e-308 1.7976931348623157e+308
Int size  8 bytes

Every numeric type carries its own limits as min and max, which is better than the header file full of macros that C needs for the same information.

Literals may be written in decimal, hex with 0x, octal with 0o, or binary with 0b, and underscores may be dropped in anywhere to group digits: 1_000_000, 0b1010_1111. The underscores are ignored.

Integer arithmetic that leaves the range of its type stops the program. It does not wrap around and it does not quietly produce garbage, which is the single largest behavioral difference between Swift arithmetic and C arithmetic. Where you want wrapping you ask for it by name.

// Swift traps on overflow. The & operators wrap instead, on purpose.
var n = Int8.max
print(n)
print(n &+ 1)
print(Int8.max.addingReportingOverflow(1))

let (sum, carried) = Int.max.addingReportingOverflow(1)
if carried {
    print("overflowed; the wrapped answer was \(sum)")
}
127
-128
(partialValue: -128, overflow: true)
overflowed; the wrapped answer was -9223372036854775808

&+, &- and &* wrap the way C does. They are for hash functions, checksums, and code that is deliberately doing modular arithmetic. The ...ReportingOverflow methods hand back both the wrapped answer and whether it wrapped, for when you want to notice without crashing.

2.3 Floating Point

Double is 64-bit and is what an unannotated literal with a decimal point becomes. Float is 32-bit and you use it when something else demands it. Float16 exists on platforms that support it. There is no decimal type in the standard library, so money belongs in an Int count of cents or in Foundation’s Decimal, never in a Double.

Floating literals may carry an exponent, 1.6e-19, and may be written in hex, 0x1p-3 for one-eighth. Division by zero gives infinity rather than a crash, and 0.0 / 0.0 gives nan, which compares unequal to everything including itself. x.isNaN is how you ask.

2.4 Bool, Character, and String

Bool is true or false and nothing else is a Bool. Swift has no truthiness: an empty array is not false, zero is not false, and nil is not false.

int_as_bool.swift:2:4: error: type 'Int' cannot be used as a boolean; test for '!= 0' instead
1 | let count = 3
2 | if count {
  |    `- error: type 'Int' cannot be used as a boolean; test for '!= 0' instead

Coming from Python or JavaScript this is the rule you will trip over most often in the first hour and miss least afterwards, because if items.isEmpty says what it means and if items never quite did.

A Character is a single character as a reader understands it, written in single or double quotes — Swift uses double quotes for both, and the type decides which you got. A String is a sequence of them. Chapter 6 is about what that costs; for now, + joins strings, += appends, == compares by content, and \(...) interpolates.

2.5 Conversions

Swift converts nothing implicitly. A conversion is written by naming the type you want and calling it, which looks like a cast and is an ordinary initializer:

// Conversions are initializers, and some of them can fail.
import Foundation

let pi = 3.9
print(Int(pi))                        // 3, truncated toward zero
print(Int(pi.rounded()))              // 4, rounded first

let big = 300
print(UInt8(truncatingIfNeeded: big)) // 44, the low eight bits

// The exact forms hand back nothing when the value will not fit.
if let small = UInt8(exactly: 200) {
    print("200 fits in a UInt8 as \(small)")
}
if UInt8(exactly: big) == nil {
    print("300 does not fit in a UInt8")
}
3
4
44
200 fits in a UInt8 as 200
300 does not fit in a UInt8

Int(someDouble) truncates toward zero and stops the program if the value is too large to represent or is a nan. Int(exactly:) hands back nothing instead of truncating, and UInt8(truncatingIfNeeded:) keeps the low bits and asks no questions. Three behaviors, three spellings, and the one you get is the one you named. C has the same three behaviors and one spelling for all of them.

The rule that values do not convert has one deliberate hole, and it is the hole that makes arithmetic bearable. A literal has no type until the expression needs one, so let x: Double = 3 and let y = 2 * someDouble both work. What fails is mixing two values that already have types, which is the error from Chapter 1. Values written down are flexible; values stored in a name are not.

2.6 Operators

The arithmetic operators are + - * / %, and % is a remainder rather than a modulus: it takes the sign of the left operand, which is C’s rule and not Python’s.

// Integer division truncates toward zero, and the remainder follows it.
print(7 / 2, 7 % 2)
print(-7 / 2, -7 % 2)
print(7 / -2, 7 % -2)
print(7.0 / 2.0, 7.0.truncatingRemainder(dividingBy: 2.0))
3 1
-3 -1
-3 1
3.5 1.0

% does not accept floating-point operands, because the answer people expect is not the answer the hardware gives; truncatingRemainder(dividingBy:) is the spelling that makes you admit what you asked for.

Comparison is == != < <= > >= and works on anything that declares it can be compared, which Chapter 10 explains. && and || short-circuit, ! negates, and all three take Bool and nothing else.

The conditional operator is C’s: let n = a > b ? a : b. Swift also has ??, which supplies a value when an optional has none, and that is Chapter 5.

Ranges are values in their own right, not loop syntax. 0..<n is half-open, 0...n is closed, a... and ...b are one-sided, and all of them can be stored in a variable, passed to a function, or asked whether they contain something with range.contains(x).

Assignment does not produce a value. if x = y is not a subtle bug in Swift, it is a compile error, and the class of mistake C spends a warning flag on does not exist here.

Exercise 2-1. Write a function that returns the number of 1 bits in an Int, and check it against the standard library’s nonzeroBitCount.

Exercise 2-2. 7 % -2 is 1 and Python says -1. Write a function mod(_:_:) that always returns a non-negative result for a positive divisor.

Exercise 2-3. Find out what Int(Double.nan) does, and what Int(exactly: Double.nan) does instead.

2.7 Tuples

A tuple gathers a few values into one without going to the trouble of declaring a type. It is the right answer for a function that has two things to say and the wrong answer for anything that outlives the call.

// A tuple gathers a few values without naming a type for them.
func extremes(_ a: [Int]) -> (low: Int, high: Int) {
    var low = a[0]
    var high = a[0]
    for n in a {
        if n < low { low = n }
        if n > high { high = n }
    }
    return (low, high)
}

let range = extremes([3, 9, 1, 14, 7])
print(range.low, range.high)
print(range.0, range.1)

let (lo, hi) = extremes([3, 9, 1, 14, 7])
print("from \(lo) to \(hi)")

let point = (x: 1.5, y: -2.0)
print(point.x + point.y)
1 14
1 14
from 1 to 14
-0.5

Elements are reached by name when the tuple has names and by position when it does not. A tuple can be taken apart in a let, and _ discards a piece you do not want: let (_, hi) = extremes(a).

Tuples compare with == and sort lexicographically, which makes (b.rank, b.name) < (a.rank, a.name) a readable two-key comparison. They cannot have methods, cannot be extended, and cannot be stored in most places that want a real type. When a tuple starts being passed between functions, promote it to a struct and give the fields real names; Chapter 7 shows what that costs, which is four lines.

2.8 When to Write the Type

Inference is not a convenience feature bolted on the side; it is how Swift is written. let count = 0 and let names = ["ada", "grace"] need no annotation and get none.

Write the type when the literal is ambiguous and you care which way it resolves, let ratio: Double = 1; when you are declaring something empty, var seen: [String] = [], because there is nothing to infer from; when the value comes from a function whose return type is not obvious at the call; and on every stored property of a type and every parameter and return, where Swift requires it anyway.

Do not write it to document the obvious. let name: String = "ada" is noise, and a reader who needs to be told that a quoted string is a String has larger problems.

A typealias gives an existing type a second name, which is worth doing when a type is long or when a name carries meaning the type does not:

typealias Celsius = Double
typealias Handler = (String) -> Void

It creates no new type and no checking. Celsius and Double are interchangeable everywhere, so this is documentation rather than safety. Chapter 7 shows how to get the safety, when you want it, by wrapping the value in a struct instead.

Exercise 2-4. Write a function that takes a number of seconds and returns a tuple of hours, minutes and seconds.

Exercise 2-5. Sort an array of (name: String, score: Int) by score descending and name ascending, using tuple comparison.

3. Control Flow

Swift’s control structures hold no surprises in their shapes. Conditions are not parenthesized, bodies are always braced, and nothing is truthy but a Bool. The two things that repay attention are guard, which changes how you lay a function out, and switch, which is the structure the rest of the language is designed around.

3.1 if

if temp > 30 {
    print("hot")
} else if temp > 15 {
    print("mild")
} else {
    print("cold")
}

The parentheses are optional and Swift programmers leave them off. The braces are not optional, not even for one statement, which retires the class of bug that made Apple’s own goto fail possible.

An if can also produce a value, which saves inventing a var and assigning to it in each branch:

let size = if n < 10 { "small" } else { "large" }

Every branch must produce a value of the same type and every case must be covered, so the else is required. switch works the same way as an expression. Where a branch needs several statements the last one is the value.

For a single condition the conditional operator is shorter still, and reads the way it does in C: let n = a > b ? a : b.

3.2 guard

guard states a condition that must hold for the rest of the function to make sense, and its else block must leave: return, throw, break, continue, or a call that never comes back.

// guard keeps the happy path flat and puts every exit at the top.
func check(_ text: String) -> String {
    guard !text.isEmpty else {
        return "no input"
    }
    guard let n = Int(text) else {
        return "\"\(text)\" is not a number"
    }
    guard n >= 0, n < 150 else {
        return "\(n) is not a plausible age"
    }
    return "age \(n)"
}

for input in ["", "abc", "-3", "41"] {
    print(check(input))
}
no input
"abc" is not a number
-3 is not a plausible age
age 41

Written with if this is four levels of nesting and the real work is at the bottom, squeezed against the right margin. Written with guard the failures are at the top in the order you think of them and the real work is at the left, and that is the shape most Swift functions take.

Two details make it work. A comma between conditions means “and”, and each one is checked in order, so n >= 0, n < 150 is a single guard. And a name bound by guard let stays in scope after the guard, for the rest of the enclosing block, which is the reason to write guard let rather than if let. Chapter 5 covers what let n = Int(text) is doing inside a condition.

3.3 Loops

while and repeat-while are C’s while and do-while under different spelling. for-in walks anything that can be walked.

// The forms a loop takes.
let names = ["ada", "grace", "alan", "edsger"]

for (i, name) in names.enumerated() {
    print(i, name)
}

for name in names where name.count > 4 {
    print("long:", name)
}

for n in stride(from: 0, to: 20, by: 6) {
    print(n, terminator: " ")
}
print()

var countdown = 3
repeat {
    print("tick \(countdown)")
    countdown -= 1
} while countdown > 0
0 ada
1 grace
2 alan
3 edsger
long: grace
long: edsger
0 6 12 18 
tick 3
tick 2
tick 1

enumerated() pairs each element with its position, and the pair is taken apart in the loop variable the way a tuple is. Reach for it rather than counting indices by hand.

A where clause on a for skips the elements that fail it, which is continue said earlier and more visibly.

stride(from:to:by:) is the loop C writes with three clauses. The to: form excludes the end and through: includes it, and the step may be negative, which is how you count down. A range cannot do that: 10...0 is a runtime error rather than a backwards range, so stride(from: 10, through: 0, by: -1) or (0...10).reversed() is how you go the other way.

3.4 break and continue

break leaves the nearest loop and continue starts its next turn. When the loop you want to leave is not the nearest one, label it:

// Labeled break: stop both loops at the first match.
let grid = [
    [3, 8, 1],
    [9, 4, 6],
    [7, 2, 5],
]

var found = "not found"

rows: for (r, row) in grid.enumerated() {
    for (c, value) in row.enumerated() {
        if value == 4 {
            found = "row \(r), column \(c)"
            break rows
        }
    }
}

print(found)
row 1, column 1

A label is a name and a colon in front of for, while, repeat or if, and break label or continue label names it. This is the one place Swift offers something that looks like a goto, it is confined to loops you can see on the screen, and it beats the flag variable that the alternative requires.

Exercise 3-1. Rewrite search without a label, returning from a function instead, and decide which you prefer.

Exercise 3-2. Print the numbers 1 to 100, replacing multiples of three with fizz, multiples of five with buzz, and multiples of both with fizzbuzz. Write it twice, once with if and once with a switch over a tuple of the two remainders.

3.5 switch

A switch compares a value against patterns and runs the first arm that matches. That much is C. Everything else about it is not.

// Classify a point by where it sits, using one switch over a tuple.
func describe(_ p: (x: Int, y: Int)) -> String {
    switch p {
    case (0, 0):
        return "the origin"
    case (_, 0):
        return "on the x axis"
    case (0, _):
        return "on the y axis"
    case let (x, y) where x == y:
        return "on the diagonal at \(x)"
    case let (x, y) where abs(x) <= 2 && abs(y) <= 2:
        return "inside the box at \(x),\(y)"
    default:
        return "somewhere else"
    }
}

for p in [(0, 0), (3, 0), (0, -4), (5, 5), (1, 2), (9, 4)] {
    print(describe(p))
}
the origin
on the x axis
on the y axis
on the diagonal at 5
inside the box at 1,2
somewhere else

Four things are happening that C’s switch cannot do.

It matches a tuple, position by position, with _ standing for any value in that position. It binds: case let (x, y) gives names to the parts of whatever matched, and those names are in scope in the arm. It filters: a where clause narrows an arm to the cases that also satisfy a condition. And it switches on things that are not integers, since strings, ranges, tuples and the enums of Chapter 8 are all matchable.

The two rules that change how you write code are these. Cases do not fall through, so there is no break to forget and no bug when you do; the fallthrough keyword exists for the rare time you want C’s behavior and it is rare. And a switch must be exhaustive. Over an Int or a String that means you need a default, which is why one is there above. Over a type with a fixed set of values it means the compiler counts the cases for you:

non_exhaustive.swift:6:5: error: switch must be exhaustive
 5 | func turn(_ d: Direction) -> String {
 6 |     switch d {
   |     |- error: switch must be exhaustive
   |     `- note: add missing case: '.west'

That message is the reason Chapter 8 argues for modeling your data as a fixed set of cases whenever the set really is fixed. Add a fifth direction a year from now and the compiler walks you to every place in the program that has to change, by file and line. No test suite gives you that and no amount of care replaces it.

Arms may list alternatives separated by commas, and may use ranges:

switch score {
case ..<0, 101...:
    print("out of range")
case 0..<60:
    print("fail")
case 60..<90:
    print("pass")
default:
    print("distinction")
}

An empty arm is not allowed. Where you want a case to do nothing you write break, which says you meant it.

Exercise 3-3. Write a function that takes an HTTP status code and returns its class as a string, using range patterns and no if.

Exercise 3-4. Rewrite describe so that the diagonal case reports whether the point is on the rising or falling diagonal, without adding an if to the body of an arm.

Exercise 3-5. Find out what fallthrough does to the value binding in the arm it falls into, and why the compiler refuses some of those cases.

4. Functions and Closures

Chapter 1 showed enough of func to write programs. This chapter finishes the declaration syntax and then spends most of its length on closures, which are functions written where they are used and are how most of Swift’s standard library is called.

4.1 Declaring a Function

Parameters may carry default values, may be variadic, and may be marked modifiable.

// Labels, defaults, variadics, and inout.
func join(_ parts: [String], with separator: String = ", ") -> String {
    var out = ""
    for (i, part) in parts.enumerated() {
        if i > 0 { out += separator }
        out += part
    }
    return out
}

func sum(of numbers: Int...) -> Int {
    var total = 0
    for n in numbers { total += n }
    return total
}

func bump(_ n: inout Int, by amount: Int = 1) {
    n += amount
}

print(join(["a", "b", "c"]))
print(join(["a", "b", "c"], with: " - "))
print(sum(of: 1, 2, 3, 4), sum())

var count = 10
bump(&count)
bump(&count, by: 5)
print(count)
a, b, c
a - b - c
10 0
16

A default value makes its argument optional at the call and lets one function stand in for the two or three overloads another language would need. Put the defaulted parameters last, because a caller supplying one still has to name it.

A variadic parameter, written with ..., arrives in the body as an array. A function may have more than one now, though a call gets ambiguous fast and one is usually enough.

inout hands the function the caller’s variable rather than a copy. The caller writes & at the call site, so the modification is visible in the source and not only in the documentation. An inout argument must be a var, and Swift copies the value in at the start and back out at the end rather than passing an address, which matters only when two arguments refer to the same variable, which the compiler will not let you do.

A function with no arrow returns Void, which is the empty tuple () wearing a friendlier name. A returned value may be ignored, and the compiler warns when you ignore one unless the function is marked @discardableResult.

Argument labels, properly

Chapter 1 introduced the two names a parameter has. The convention that makes Swift read the way it does is that the call site should form a phrase, with the function’s base name carrying the verb and the labels carrying the prepositions. join(parts, with: ", "), sum(of: 1, 2, 3), bump(&count, by: 5). Where the first argument’s role is already obvious from the base name, _ suppresses the label, which is why join takes _ parts and not parts parts.

The labels are part of the function’s name. bump(_:by:) is what that function is called, and a bump(_:to:) beside it is a different function rather than a conflict. This is why Swift needs no overload resolution tricks to have several functions that do related things.

4.2 Functions as Values

A function has a type, written as its parameter types, an arrow, and its return type. Labels are not part of a function type, only of a function’s name, so (Int) -> Int describes twice and square both.

// Functions are values: they have types, and they can be passed and stored.
func twice(_ n: Int) -> Int { n * 2 }
func square(_ n: Int) -> Int { n * n }

func applyAll(_ n: Int, _ steps: [(Int) -> Int]) -> Int {
    var value = n
    for step in steps {
        value = step(value)
    }
    return value
}

let pipeline: [(Int) -> Int] = [twice, square, { $0 - 1 }]
print(applyAll(3, pipeline))

// A nested function is a closure with a name.
func tally(_ words: [String]) -> String {
    var total = 0
    func add(_ w: String) {
        total += w.count
    }
    for w in words {
        add(w)
    }
    return "\(words.count) words, \(total) characters"
}

print(tally(["the", "quick", "brown", "fox"]))
35
4 words, 16 characters

Naming a function without calling it gives you the function itself, so twice goes into the array beside a closure written inline and nothing distinguishes them afterwards. When two functions share a base name, the full name disambiguates: bump(_:by:).

A function declared inside another function is a closure that happens to have a name. add can see and modify total, which is what makes the nested form worth using: the alternative is a parameter and a return value threaded through every call, to say something the enclosing function already knows.

4.3 Closures

A closure is a function without a name, written { parameters in body }. Swift lets you drop everything it can work out for itself, and the standard library is designed on the assumption that you will.

// The same closure, written five ways, each shorter than the last.
let names = ["edsger", "ada", "grace", "alan"]

func byLength(_ a: String, _ b: String) -> Bool {
    return a.count < b.count
}
print(names.sorted(by: byLength))

print(names.sorted(by: { (a: String, b: String) -> Bool in return a.count < b.count }))
print(names.sorted(by: { a, b in a.count < b.count }))
print(names.sorted(by: { $0.count < $1.count }))
print(names.sorted { $0.count < $1.count })

All five print the same line:

["ada", "alan", "grace", "edsger"]

The steps are worth knowing separately, because you will read code written at every one of them. Types go first, since sorted(by:) already says its comparator takes two Strings and returns a Bool. return goes next, because a single-expression closure returns its expression. Then the parameter names themselves, replaced by $0, $1 and so on in order. Last, when the closure is the final argument, it moves outside the parentheses and the label disappears: that is trailing closure syntax, and it is why so much Swift ends in a brace.

A function taking more than one closure can take them all trailing, with the first unlabeled and the rest labeled:

animate(duration: 0.3) {
    view.alpha = 0
} completion: { finished in
    view.removeFromSuperview()
}

Stop shortening before the code stops being readable. $0 < $1 is clear; $0.1.count > $1.1.count is a puzzle, and the version with named parameters is the one to write.

Exercise 4-1. Write applyAll so it takes the steps as a variadic parameter instead of an array, and see what the call looks like.

Exercise 4-2. Sort an array of strings case-insensitively, then by length with ties broken alphabetically.

4.4 Capture

A closure captures the variables it mentions from the scope around it, and it keeps them alive for as long as the closure lives.

// A closure keeps the variables it captured alive.
func makeCounter(startingAt start: Int) -> () -> Int {
    var n = start
    return {
        n += 1
        return n
    }
}

let next = makeCounter(startingAt: 0)
let other = makeCounter(startingAt: 100)

print(next(), next(), next())
print(other(), other())
print(next())
1 2 3
101 102
4

n is a local variable of makeCounter, and makeCounter has returned by the time anything calls the closure. The variable is still there because the closure captured it, and each call to makeCounter makes a fresh one, which is why next and other count independently.

Capture is by reference, not by value: the closure sees the variable, not a snapshot of it, and assigning to n inside the closure changes the same n the enclosing function would see. When you want the snapshot, say so with a capture list:

var total = 0
let report = { [total] in print(total) }
total = 99
report()      // prints 0

The square brackets before in capture each named value by copying it at the moment the closure is created. That is also where [weak self] goes, which Chapter 7 explains along with the reference cycles it prevents.

One consequence catches everybody once. Closures are reference types. Assigning a closure to a second name gives you two names for one closure, sharing the captured state, which is the opposite of how every other value in this chapter behaves and is the subject of Chapter 7.

4.5 Escaping

A closure that outlives the call it was passed to has escaped. Swift assumes a function parameter’s closure does not escape, which lets it skip the bookkeeping that keeping one alive requires, and it checks.

escaping.swift:2:21: error: converting non-escaping parameter 'handler' to generic parameter 'Element' may allow it to escape
1 | func register(_ handler: () -> Void, into handlers: inout [() -> Void]) {
2 |     handlers.append(handler)
  |                     `- error: converting non-escaping parameter 'handler' to generic parameter 'Element' may allow it to escape

Storing the closure in an array is the case the rule exists for: the array outlives register, so the closure does too. Marking the parameter @escaping tells the compiler to expect it:

func register(_ handler: @escaping () -> Void, into handlers: inout [() -> Void]) {
    handlers.append(handler)
}

A closure that escapes must say self. explicitly when it refers to a property, which is Swift making a capture of self visible rather than letting it happen quietly. That visibility is the point, and Chapter 7 shows what it is protecting you from.

Exercise 4-3. Write makeAccumulator() that returns a closure taking an Int and returning the running total of everything it has been given.

Exercise 4-4. Rewrite counter.swift using a capture list to copy start, and explain why the program then behaves differently.

Exercise 4-5. Write a function that takes a closure and returns a closure that calls it and prints how long it took. Use it to time a slow function.

5. Optionals

Tony Hoare called the null reference his billion-dollar mistake. Swift’s answer is to delete it. There is no null, no nil pointer, no undefined, and no value of type String that is secretly absent. What Swift has instead is a separate type for “a String, or nothing”, and a compiler that will not let you confuse the two.

This is the feature you will meet most often and the one that makes Swift feel strict at first and quiet later. A whole category of crash stops being possible, and the price is that you say what should happen when a value is missing, at the place where it might be missing, once.

5.1 The Type of Nothing

Any type T has a partner written T?, which holds either a T or nothing. The nothing is spelled nil. String? is a different type from String, and nowhere will one be accepted for the other:

optional_math.swift:3:7: error: value of optional type 'Int?' must be unwrapped to a value of type 'Int'
1 | let a = Int("3")
2 | let b = 4
3 | print(a + b)
  |       |- note: coalesce using '??' to provide a default when the optional value contains 'nil'
  |       `- note: force-unwrap using '!' to abort execution if the optional value contains 'nil'

Int("3") returns Int? because Int("banana") has to return something, and the something is nil. Every function in Swift that can fail to produce a value says so in its return type: readLine(), array.first, dictionary[key], Int(someString). You never have to ask the documentation whether a call can come back empty. The type says.

nil is not zero, not an empty string, and not a pointer. It is one of the two cases of the type, and it is available for value types as well as objects, so Int? is as ordinary as String?.

5.2 Getting the Value Out

The main form is if let, which tests and unwraps in one move and gives you the unwrapped value inside the block:

if let n = Int(text) {
    print(n * 2)
} else {
    print("not a number")
}

When the new name would repeat the old one, drop the right-hand side:

if let text {
    print(text.count)
}

Inside the braces, text is a String rather than a String?, shadowing the outer name for the length of the block. Several bindings and ordinary conditions may be chained with commas, checked left to right, and later ones may use the names bound by earlier ones:

if let a = Int(first), let b = Int(second), a < b {
    print(a + b)
}

guard let does the same thing and leaves the name in scope for the rest of the enclosing block, which is what you want when the missing value means the function has nothing to do. while let loops until the value is missing, which is what Chapter 1’s readLine loop was doing.

// Add up the numbers on the input, and say which lines were not numbers.
import Foundation

var total = 0
var bad: [String] = []

while let line = readLine() {
    let text = line.trimmingCharacters(in: .whitespaces)
    if text.isEmpty {
        continue
    }
    guard let n = Int(text) else {
        bad.append(text)
        continue
    }
    total += n
}

print("total \(total)")
if !bad.isEmpty {
    print("ignored \(bad.count): \(bad.joined(separator: " "))")
}
$ ./sumlines < sumlines.in
total 37
ignored 1: seven

Three optionals are handled in eighteen lines and none of them is checked against nil by hand. The loop ends when the input does, the bad line is collected rather than crashing the program, and n exists only where it means something.

5.3 Supplying a Default

?? produces the wrapped value if there is one and its right-hand side if there is not.

// Supplying a value when there is none, and reaching through nothing.
let settings = ["editor": "nano", "shell": "bash"]

let editor = settings["editor"] ?? "vi"
let pager = settings["pager"] ?? "less"
print(editor, pager)

// ?. stops the whole chain the moment anything is missing.
print(settings["editor"]?.uppercased() ?? "unset")
print(settings["pager"]?.uppercased() ?? "unset")

let words = ["alpha", "beta"]
print(words.first?.count ?? 0)
print([String]().first?.count ?? 0)

// ?? chains, and the right side is only evaluated if it is needed.
let names: [String] = []
print(names.first ?? settings["shell"] ?? "nobody")
nano less
NANO
unset
5
0
bash

The right-hand side is not evaluated unless it is needed, so cache[key] ?? expensiveLookup(key) does the work only on a miss. ?? also chains, and the result of a ?? b is non-optional when b is, which is what makes it the normal way to leave optional-land.

5.4 Optional Chaining

?. calls a method or reads a property only if there is something to call it on, and produces nothing otherwise. The result is optional, because the whole chain might have produced nothing, and a chain of any length fails as a unit: a?.b?.c.d is nil the moment any link is nil, and nothing further is evaluated.

That is the difference between ?. and !. Both reach through an optional; ?. gives up quietly and ! crashes.

5.5 Force Unwrapping

! takes the value out and stops the program if there is not one.

fu.swift:2: Fatal error: Unexpectedly found nil while unwrapping an Optional value

It is the right tool in two situations and the wrong tool everywhere else. The first is when the surrounding code has already established that the value is there, as in Chapter 1’s c.wholeNumberValue! under a case "0"..."9". The second is when a missing value means the program is broken beyond recovery and crashing immediately, at the line that noticed, is better than limping on: a resource that is compiled into the program, a constant regular expression that either parses or the build is wrong.

Everywhere else it is a crash you have chosen to write. When you find yourself reaching for ! because the compiler is in your way, the compiler is the one that is right.

try! and as! are the same bargain for errors and for casts, and Chapters 9 and 10 give them the same advice.

5.6 An Optional Is an Enum

None of this is built into the compiler as special syntax. Optional is an ordinary enum from Chapter 8, with two cases, and the rest is sugar:

enum Optional<Wrapped> {
    case none
    case some(Wrapped)
}

Which means a switch works on it, and everything a switch can do is available:

// An Optional is an ordinary enum with two cases, and switches like one.
for text in ["42", "seven", "-1"] {
    switch Int(text) {
    case .some(let n) where n < 0:
        print("\(text) is negative")
    case let n?:
        print("\(text) is \(n)")
    case nil:
        print("\(text) is not a number")
    }
}

// map applies a function to the value inside and leaves nothing alone.
let maybe = Int("21")
print(maybe.map { $0 * 2 } ?? 0)
print(Int("x").map { $0 * 2 } ?? 0)

// flatMap collapses the optional you would otherwise get twice over.
let ages = ["ada": "36", "grace": "unknown"]

func age(of name: String) -> Int? {
    ages[name].flatMap { Int($0) }
}

print(age(of: "ada") ?? -1, age(of: "grace") ?? -1, age(of: "alan") ?? -1)
42 is 42
seven is not a number
-1 is negative
42
0
36 -1 -1

case let n? is shorthand for case .some(let n), and case nil for case .none. Use the switch when the two branches are both real work and the if let when one of them is a fallback.

map transforms what is inside without opening it: nothing in, nothing out. flatMap does the same for a transformation that itself returns an optional, collapsing the Int?? you would otherwise be holding. age(of:) is the case this exists for, since the dictionary lookup may fail and the parse may fail independently, and the caller cares about neither distinction.

Exercise 5-1. Rewrite age(of:) with guard let and no flatMap, then decide which you would rather read.

Exercise 5-2. Write a function taking [String] and returning [Int] containing only the entries that parse. Then find the standard library call that does it in one line.

Exercise 5-3. settings["pager"]?.uppercased() has type String?. What is the type of settings["pager"]?.count, and what is the type of (settings["pager"]?.count) ?? 0?

5.7 Implicitly Unwrapped Optionals

var x: Int! declares an optional that unwraps itself whenever it is used, crashing if it is nil. It exists for values that are genuinely absent for a short window between construction and first use, which in practice means views loaded from an interface file on Apple platforms.

Write it when a framework hands you one. Do not introduce one. It is a force unwrap that has been moved away from the line where it will crash, which is the opposite of what you want.

5.8 Habits

An optional means the value can be absent, so an optional whose absent case is impossible is a lie that everyone downstream has to keep handling. If a function always returns a value, say so in its type; the optional is not free caution.

An empty array is not nil and [String]? is almost always a mistake, since the empty case already says “nothing here” and now there are two ways to say it and every caller must handle both. The same goes for String? where "" would do, unless the difference between empty and absent is real and you can say what it is.

Unwrap early. Take the optionals apart at the edge of your program, where the input arrives, and let everything inside work with values that exist. A codebase that passes optionals into its core is a codebase that checks for nil in fifty places, which is the state Swift was designed to get you out of.

Exercise 5-4. Find three functions in the standard library that return an optional, and say in one sentence each what the nil means.

Exercise 5-5. Take a program you wrote in another language that checks for null, and work out which of those checks Swift would not require.

6. Collections and Strings

Swift has three collections in the standard library and one string type, and all four behave as values: assigning one copies it, and passing one to a function hands over a copy. Chapter 7 explains how that is affordable. This chapter is about using them.

6.1 Arrays

An array is ordered, indexed from zero, and grows as needed. Its type is [Element], which is sugar for Array<Element>.

// Arrays, and the calls you will use on them every day.
var primes = [2, 3, 5, 7, 11]
primes.append(13)
primes.insert(1, at: 0)
primes.remove(at: 0)

print(primes.count, primes.first ?? 0, primes.last ?? 0)
print(primes.filter { $0 % 3 != 0 })
print(primes.map { $0 * $0 })
print(primes.reduce(0, +))
print(primes.contains(7), primes.firstIndex(of: 7) ?? -1)
print(primes.sorted(by: >))

// A slice shares the original's storage and keeps its positions.
let middle = primes[1..<4]
print(middle, middle.startIndex, middle.count)
print(Array(middle))
6 2 13
[2, 5, 7, 11, 13]
[4, 9, 25, 49, 121, 169]
41
true 3
[13, 11, 7, 5, 3, 2]
[3, 5, 7] 1 3
[3, 5, 7]

first and last are optional because the array may be empty, and so is firstIndex(of:) because the element may be absent. Subscripting is not: an index out of range stops the program, on the theory that you knew the index was valid and were wrong about something larger.

map, filter and reduce are the three you will use constantly. reduce takes a starting value and a function of two arguments, and + passed as a function is the whole of reduce(0, +). There is a reduce(into:) for building up a collection, which avoids copying the accumulator on each step.

The slice is the piece worth care. primes[1..<4] is an ArraySlice, not an Array. It shares the original’s storage rather than copying, and it keeps the original’s index numbers, so middle.startIndex is 1 and middle[0] would crash. This is right for a value you are about to walk over and wrong for a value you are about to keep, and Array(middle) is how you stop sharing. The same is true of every slice type in Swift.

Exercise 6-1. primes.reduce(0, +) sums. Write the version that multiplies, and the version that finds the largest without using max().

Exercise 6-2. Write a function that returns the first index where two arrays differ, or nil if they are equal.

6.2 Dictionaries

A dictionary maps keys to values, its type is [Key: Value], and the key type must be hashable, which every standard type is. Its subscript returns an optional, because the key may be absent, and that single decision removes the whole family of bugs that comes from a lookup quietly returning a zero.

// The most common words on the input, longest count first.
import Foundation

var counts: [String: Int] = [:]

while let line = readLine() {
    for word in line.lowercased().split(whereSeparator: { !$0.isLetter }) {
        counts[String(word), default: 0] += 1
    }
}

let ranked = counts.sorted { a, b in
    a.value == b.value ? a.key < b.key : a.value > b.value
}

for (word, n) in ranked.prefix(5) {
    print(String(format: "%4d %@", n, word))
}
$ ./wordfreq < gettysburg.txt
   3 and
   3 nation
   2 a
   2 are
   2 conceived

counts[key, default: 0] += 1 is the counting idiom. The subscript with a default: reads as the default when the key is absent and, assigned to, inserts it, so the two-line “check then insert” disappears.

Assigning nil to a key removes it. Iterating gives (key, value) pairs in an order that is not the insertion order and not the sorted order and is not guaranteed to be the same between two runs of the same program, which is why ranked sorts before printing. Any output that iterates a dictionary or a set and does not sort is a test that fails intermittently.

sorted(by:) on a dictionary produces an array of pairs. The comparator here sorts by count descending and breaks ties alphabetically, which is what makes the output above reproducible.

6.3 Sets

A Set holds each value at most once, answers contains in constant time, and has the operations you would expect.

// A set holds each value once and answers membership fast.
let a: Set = [1, 2, 3, 4]
let b: Set = [3, 4, 5]

print(a.union(b).sorted())
print(a.intersection(b).sorted())
print(a.subtracting(b).sorted())
print(a.symmetricDifference(b).sorted())
print(a.contains(3), a.isSubset(of: [1, 2, 3, 4, 5]))

// The order a set iterates in is not the order you wrote it.
var seen: Set<String> = []
var uniqueInOrder: [String] = []
for word in ["the", "cat", "sat", "on", "the", "mat", "the", "end"] {
    if seen.insert(word).inserted {
        uniqueInOrder.append(word)
    }
}
print(uniqueInOrder)
[1, 2, 3, 4, 5]
[3, 4]
[1, 2]
[1, 2, 5]
true true
["the", "cat", "sat", "on", "mat", "end"]

A set literal looks like an array literal, so the type annotation is what decides which you get. insert returns a pair saying whether the value was new and what is now stored, and testing .inserted is the way to deduplicate while keeping the original order, which a set alone cannot do.

6.4 Strings

A Swift String is a sequence of Character values, where a Character is what a reader would point at: a base letter together with any marks that attach to it, which Unicode calls an extended grapheme cluster. That decision is the reason the string API looks unfamiliar, and it is worth ten minutes.

// A string is characters, and a character is not a byte.
let s = "café 🇨🇭"
print(s.count, s.unicodeScalars.count, s.utf8.count, s.utf16.count)

// The same text composed two ways is one string as far as == is concerned.
let precomposed = "caf\u{e9}"
let decomposed = "cafe\u{301}"
print(precomposed == decomposed, precomposed.count, decomposed.count)
print(precomposed.utf8.count, decomposed.utf8.count)

// Positions are String.Index, not Int.
let space = s.firstIndex(of: " ")!
print(s[..<space], s[s.index(after: space)...])

// Slicing gives a Substring, which shares the original's storage.
let name = "Ada Lovelace"
let firstWord = name.prefix(while: { $0 != " " })
print(firstWord, type(of: firstWord))
print(String(firstWord).uppercased())

// Multiline and raw strings.
let usage = """
    usage: wf [-n count] [file]
      -n  how many words to show
    """
print(usage)
print(#"a \n stays a backslash and an n in a raw string"#)
6 7 14 9
true 4 4
5 6
café 🇨🇭
Ada Substring
ADA
usage: wf [-n count] [file]
  -n  how many words to show
a \n stays a backslash and an n in a raw string

Read the first line. "café 🇨🇭" is six characters, seven Unicode scalars, and fourteen bytes of UTF-8, because the flag is one character made of two scalars made of eight bytes. Every one of those numbers is the right answer to a different question, and Swift makes you ask the one you meant rather than guessing from a single length.

The second and third lines are the payoff. Two strings that a user would call identical compare equal even though one is five bytes and the other is six, because == compares by canonical equivalence and not by bytes. In most languages that comparison is false and the bug surfaces when a name typed on a Mac fails to match the same name typed on Windows.

The price is that you cannot index a string with an integer:

string_int_index.swift:2:7: error: 'subscript(_:)' is unavailable: cannot subscript String with an Int, use a String.Index instead.
1 | let name = "Ada Lovelace"
2 | print(name[0])

There is no cheap way to find the tenth character of a string whose characters have different widths, so Swift refuses to offer one that pretends otherwise. Positions are String.Index values, obtained from the string itself with firstIndex(of:), index(after:), index(_:offsetBy:) and friends, and used to slice with s[a..<b]. In practice you reach for indices far less than you expect, because split, prefix, hasPrefix, contains and firstRange(of:) in the standard library, and replacingOccurrences and trimmingCharacters once you import Foundation, cover most of what index arithmetic used to do.

Slicing gives a Substring, which shares the original’s storage the way an ArraySlice does and keeps the whole original alive as long as it exists. Convert with String(...) when you are going to keep it.

The multiline form runs between """ on their own lines, and the indentation of the closing delimiter is stripped from every line, so the text lines up with your code and not against the left margin. A raw string, #"..."#, turns off backslash escapes, which is what you want for regular expressions and Windows paths; interpolation inside one is written \#(x).

Exercise 6-3. Write a function that reverses the words in a line without reversing the characters in each word.

Exercise 6-4. Count how many times each character occurs in the input. Then change it to count Unicode scalars instead, and find input where the two answers differ.

Exercise 6-5. wordfreq splits on anything that is not a letter, so “don’t” becomes two words. Fix it, and decide what you want to do about hyphens.

6.5 Choosing

An array when order matters or duplicates are meaningful. A set when you are asking “have I seen this” and the answer is all you need. A dictionary when one value is looked up by another. Those are the same three decisions you make in any language.

What is different here is that all three are values, so a collection handed to a function cannot be modified behind your back, and a collection stored in two places is two collections. That eliminates the most common source of action at a distance in a large program, and Chapter 7 is about what it costs and why the answer is usually nothing.

7. Structs, Classes, and Value Semantics

Swift gives you two ways to define a type. A struct is a value: assigning it copies it. A class is an object: assigning it makes a second name for the same thing. Everything else in this chapter follows from that one sentence, and so does most of the difference between a Swift program that behaves and one that does not.

The languages you are coming from mostly do not offer the choice. In Python and JavaScript every object of your own making is a reference, and the copying kind exists only for numbers and strings. Swift puts the decision in your hands and expects you to take the value side by default.

7.1 The Difference, Demonstrated

// The same program twice: once with a struct, once with a class.
struct PointValue {
    var x: Int
    var y: Int
}

final class PointObject {
    var x: Int
    var y: Int
    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
}

var a = PointValue(x: 1, y: 1)
var b = a
b.x = 99
print("struct:", a.x, b.x)

let c = PointObject(x: 1, y: 1)
let d = c
d.x = 99
print("class: ", c.x, d.x)
print("same object:", c === d)
struct: 1 99
class:  99 99
same object: true

b = a made a second point. d = c made a second name. That is the entire distinction, and every other difference between the two is a consequence of it.

Two consequences show up in the code above. The struct got an initializer for free, generated from its stored properties, while the class had to be given one, because a subclass might add properties the memberwise form would not know about. And c and d are declared let, yet d.x = 99 is allowed: the constant is the reference, not the object it points at. For a struct, let means the value cannot change at all.

That last point is where a let in Swift stops resembling const in C. A let struct is deeply immutable, down through every struct and array and string inside it. A let class reference is a constant arrow pointing at something that anybody holding it can rearrange.

7.2 Structs

A struct has stored properties, computed properties, and methods. A method that changes a stored property must say mutating, which tells the caller as well as the compiler.

// A struct with stored properties, computed properties, and mutating methods.
struct Account {
    let owner: String
    private(set) var balance: Int
    var history: [String] = []

    var isOverdrawn: Bool {
        balance < 0
    }

    var summary: String {
        "\(owner): \(balance) cents" + (isOverdrawn ? " (overdrawn)" : "")
    }

    init(owner: String, opening: Int = 0) {
        self.owner = owner
        self.balance = opening
    }

    mutating func deposit(_ amount: Int) {
        balance += amount
        history.append("+\(amount)")
    }

    mutating func withdraw(_ amount: Int) {
        balance -= amount
        history.append("-\(amount)")
    }
}

var acct = Account(owner: "ada", opening: 100)
acct.deposit(50)
acct.withdraw(200)

print(acct.summary)
print(acct.history)

let frozen = acct
acct.deposit(1000)
print(frozen.balance, acct.balance)
ada: -50 cents (overdrawn)
["+50", "-200"]
-50 950

frozen was taken before the last deposit and still holds what the account looked like then. Nothing was copied by hand and nothing can reach back and change it. That is the property this chapter is selling: a value handed to somebody else is a fact about a moment, not a window onto whatever happens next.

A computed property is a function that reads like a field. isOverdrawn and summary store nothing and run each time they are read, and the single expression is the return value the way it is in a one-line function. Add a set block beside the get and it becomes writable; write only the body and you have a read-only property, which is the common case.

private(set) makes balance readable from anywhere and writable only inside the type, which is how you offer a fact without offering a lever. Access control in full is private, fileprivate, internal (the default, meaning the whole module), package, and public, plus open for classes that may be subclassed outside their module. Chapter 11 says where module boundaries come from.

Writing any initializer of your own suppresses the free memberwise one. Put your initializer in an extension instead and you keep both, which is a trick worth knowing early.

The two errors you will make are these:

missing_mutating.swift:3:21: error: left side of mutating operator isn't mutable: 'self' is immutable
3 |     func bump() { n += 1 }
  |     |               `- error: left side of mutating operator isn't mutable: 'self' is immutable
  |     `- note: mark method 'mutating' to make 'self' mutable
mutating_let.swift:7:9: error: cannot use mutating member on immutable value: 'counter' is a 'let' constant
6 | let counter = Counter()
  | `- note: change 'let' to 'var' to make it mutable
7 | counter.bump()

The second is the first one’s point arriving at the call site. A mutating method is one that replaces the value, so it may only be called on something that is allowed to be replaced.

Property observers run on assignment to a stored property:

var level: Int = 0 {
    willSet { print("about to become \(newValue)") }
    didSet { if level > 10 { level = 10 } }
}

They are useful for clamping and for keeping a derived value in step, and they are a place people put too much behavior. A didSet that calls three other things is a method wearing a disguise.

static members belong to the type rather than an instance, which is where constants and factory functions go. static let shared is also how a singleton is spelled, for the rare case that is the right answer.

7.3 Classes

Use a class when the thing you are modeling has an identity that is separate from its contents. Two accounts with the same balance are the same value; two network connections with the same configuration are not the same connection. That is the test, and it decides most cases on its own.

The other reasons are narrower. You need inheritance, and a subclass really is a substitutable specialization rather than a shortcut for sharing code. You need a deinit to release something at a known moment. You are talking to a framework that requires one.

Classes bring inheritance, override, super, and the initializer rules that go with them: a designated initializer must set every property it declares and then call super.init, a convenience initializer must delegate across to another in the same class. Mark a class final when nothing should subclass it, which is most classes, and the compiler can then call its methods directly rather than through a table.

=== asks whether two references point at the same object, which is a question that only makes sense for classes and is why == is not the same thing.

Reference counting

Swift frees an object when the last reference to it goes away. The counting is inserted by the compiler rather than done by a background collector, so deallocation happens at a predictable point and deinit runs then.

The failure mode is a cycle. Two objects that refer to each other keep each other’s count above zero, and neither is ever freed.

// Two objects pointing at each other, and the word that lets them go.
final class Room {
    let name: String
    var occupant: Guest?
    init(_ name: String) { self.name = name }
    deinit { print("freed room \(name)") }
}

final class Guest {
    let name: String
    var room: Room?
    init(_ name: String) { self.name = name }
    deinit { print("freed guest \(name)") }
}

final class Suite {
    let name: String
    var occupant: Tenant?
    init(_ name: String) { self.name = name }
    deinit { print("freed suite \(name)") }
}

final class Tenant {
    let name: String
    weak var suite: Suite?
    init(_ name: String) { self.name = name }
    deinit { print("freed tenant \(name)") }
}

func leaks() {
    let room = Room("101")
    let guest = Guest("ada")
    room.occupant = guest       // strong
    guest.room = room           // strong, and now neither can ever reach zero
    print("leaving leaks()")
}

func doesNot() {
    let suite = Suite("102")
    let tenant = Tenant("grace")
    suite.occupant = tenant     // strong
    tenant.suite = suite        // weak, so the count never rises
    print("leaving doesNot()")
}

leaks()
print("---")
doesNot()
print("---")
leaving leaks()
---
leaving doesNot()
freed suite 102
freed tenant grace
---

leaks() prints no deinit at all. The room and the guest are unreachable the moment the function returns and they are still in memory when the program ends. Nothing warns you; the only symptom is that a long-running program grows.

A weak reference does not raise the count. It is optional and automatically becomes nil when the object goes away, which is why weak var and never weak let. unowned also does not raise the count and is not optional, so it is faster and crashes if you are wrong; use it only when the referred-to object is guaranteed to outlive the referrer, as a child pointing back at a parent that owns it.

The rule that covers nearly every case: the owner holds strong, the owned holds back weakly. A parent owns its children; a child refers weakly to its parent. A closure stored on an object and referring to that object needs [weak self] in its capture list, which is the single most common cycle in real code and the reason Swift makes escaping closures spell out self.

7.4 Copying Is Cheaper Than It Sounds

If arrays are values, and an array of a million elements is assigned, has a million elements been copied? No. Swift’s collections share their storage and copy it only when somebody writes to one of the copies.

// Copying an array is free until somebody writes to one of the copies.
func storage(_ a: [Int]) -> UInt {
    a.withUnsafeBufferPointer { UInt(bitPattern: $0.baseAddress) }
}

var a = [1, 2, 3]
var b = a
print("after assignment, shared:", storage(a) == storage(b))

b.append(4)
print("after b changed, shared:", storage(a) == storage(b))
print(a, b)
after assignment, shared: true
after b changed, shared: false
[1, 2, 3] [1, 2, 3, 4]

That is copy-on-write, and String, Array, Dictionary and Set all do it. You get the behavior of copying, which is simple to reason about, and you pay for a copy only where one is needed. A struct of your own holding those types inherits the trick for free, since the copy of the struct copies the references inside it and the storage stays shared.

You can build the same behavior into your own type with isKnownUniquelyReferenced(&storage), which is what you do when wrapping a class-based buffer in a value-shaped API. It is a rare need and the standard library has done it for you everywhere it counts.

7.5 Choosing

Start with struct. Reach for class when identity matters, when you need inheritance, or when a framework demands it. This is the reverse of the default in most object-oriented languages and it is the advice Apple’s own API design guidance gives.

What you get for it is local reasoning. A function that takes a struct cannot change the caller’s copy, so reading the call tells you what can happen without reading the function. Two threads holding the same value are holding two values and cannot race over it, which Chapter 12 leans on heavily. And a value stored in a dictionary or captured in a closure is the value as it was, not a window onto whatever it becomes.

What you give up is shared mutable state, which you wanted less often than you think.

Exercise 7-1. Add a mutating func transfer(_:to:) to Account taking the other account as inout. It compiles. Now call it with the same account on both sides, read the error, and say what it is protecting.

Exercise 7-2. Change PointValue to a class in copyshare.swift and find every line of output that changes.

Exercise 7-3. Write a Deck struct holding an array of cards, with a mutating func shuffle(). Then hand a deck to a function that shuffles it and confirm the caller’s deck is untouched.

Exercise 7-4. Fix leaks() by making one of the two references weak, and decide which one it should be.

Exercise 7-5. Add a closure property to Room that prints the room’s name, assign it inside init without a capture list, and confirm from the deinit output that the object now leaks by itself.

8. Enums and Pattern Matching

An enum in C is a set of names for integers. An enum in Swift is a type whose value is one of a fixed set of cases, and a case may carry values of its own. That second half turns it into something C has no word for and Lux calls a sum type: a value that is one shape or another, never both, never neither.

This is the feature Swift is built around. Optional is one. Result is one. Most of the types worth defining in a program turn out to be one.

8.1 Cases and Raw Values

The simple form names a set of alternatives, and the type may be given a raw value so that it can cross the boundary into a file or a network.

// An enum with raw values is a name for a value the outside world uses.
enum Level: Int, CaseIterable {
    case debug = 10
    case info = 20
    case warning = 30
    case error = 40

    var label: String {
        "\(self)".uppercased()
    }

    var isNoisy: Bool {
        self <= .info
    }
}

extension Level: Comparable {
    static func < (a: Level, b: Level) -> Bool {
        a.rawValue < b.rawValue
    }
}

for level in Level.allCases {
    print(level.rawValue, level.label, level.isNoisy)
}

// Coming back from a raw value can fail, so it hands back an optional.
print(Level(rawValue: 30)?.label ?? "unknown")
print(Level(rawValue: 35)?.label ?? "unknown")
10 DEBUG true
20 INFO true
30 WARNING false
40 ERROR false
WARNING
unknown

Several things are worth taking from that.

An enum can have methods and computed properties, which surprises people who arrive expecting a list of constants. isNoisy belongs on the type because it is a fact about levels, and putting it there means it cannot drift out of step with a copy of the same logic somewhere else.

CaseIterable gives you allCases, in declaration order, for free. The compiler writes it, and it stays right when you add a case.

The raw value is not the case. Level.warning is a Level; 30 is an Int that happens to represent it. Going from the case to the raw value always works and is written .rawValue; coming back may fail, so Level(rawValue:) is a failable initializer returning Level?. Strings work as raw values too, and with no values given, the raw value of each case is its own name, which is how most parsing enums are written.

Inside the type, .debug may be written without the type name wherever the type is already known, which is why self <= .info reads as it does.

8.2 Associated Values

A case may carry values, and different cases may carry different ones. That is what makes an enum a type rather than a list of names.

// One type, several shapes, each carrying only what its own case needs.
enum Command {
    case quit
    case move(direction: String, steps: Int)
    case say(String)
    case unknown(String)
}

func parse(_ line: String) -> Command {
    let parts = line.split(separator: " ").map(String.init)
    switch parts.first {
    case "quit", "q":
        return .quit
    case "go":
        guard parts.count == 3, let steps = Int(parts[2]) else {
            return .unknown(line)
        }
        return .move(direction: parts[1], steps: steps)
    case "say":
        return .say(parts.dropFirst().joined(separator: " "))
    default:
        return .unknown(line)
    }
}

func run(_ c: Command) -> String {
    switch c {
    case .quit:
        return "bye"
    case .move(let direction, let steps) where steps > 10:
        return "that is too far to go \(direction)"
    case .move(let direction, let steps):
        return "going \(direction) \(steps) step\(steps == 1 ? "" : "s")"
    case .say(let text) where text.isEmpty:
        return "you say nothing"
    case .say(let text):
        return "you say \"\(text)\""
    case .unknown(let line):
        return "I do not know how to \"\(line)\""
    }
}

for line in ["go north 3", "go north 1", "go up 99", "say hello there", "say", "dance", "q"] {
    print(run(parse(line)))
}
going north 3 steps
going north 1 step
that is too far to go up
you say "hello there"
you say nothing
I do not know how to "dance"
bye

Command is one type with four shapes. A .quit carries nothing, a .move carries a direction and a count, a .say carries a line of text. There is no Command with a steps and no direction, no Command that is both a move and a quit, and no field that is meaningful for one case and ignored for the others.

Compare the alternative. A struct with an optional direction, an optional step count, an optional message and a kind tag can represent all of those nonsense combinations, and every function reading it has to decide what to do about them. The enum makes the nonsense unspellable, which means nobody has to write the code that handles it.

switch over the enum is exhaustive, so when you add a .look case next month the compiler stops at every function that has to change and names the file and line. That is the argument for this design in one sentence. A missed case is a compile error rather than a bug report.

Note parts.first in parse: it is a String?, and the switch matches it against string literals directly, because an optional is an enum and a literal is a pattern for the .some case. The default catches nil along with everything else.

Exercise 8-1. Add a .look case to Command and let the compiler show you every place that needs changing.

Exercise 8-2. Rewrite Command as a struct with optional fields, write the same run function against it, and count the impossible states you had to handle.

8.3 Recursive Enums

A case cannot normally hold a value of its own type, since that would make the size of the type infinite. indirect puts a level of pointer between them and the problem goes away.

// A tiny expression tree. `indirect` lets a case hold its own type.
indirect enum Expr {
    case number(Double)
    case add(Expr, Expr)
    case subtract(Expr, Expr)
    case multiply(Expr, Expr)
    case divide(Expr, Expr)
    case negate(Expr)
}

func evaluate(_ e: Expr) -> Double {
    switch e {
    case .number(let n):
        return n
    case .add(let a, let b):
        return evaluate(a) + evaluate(b)
    case .subtract(let a, let b):
        return evaluate(a) - evaluate(b)
    case .multiply(let a, let b):
        return evaluate(a) * evaluate(b)
    case .divide(let a, let b):
        return evaluate(a) / evaluate(b)
    case .negate(let a):
        return -evaluate(a)
    }
}

func show(_ e: Expr) -> String {
    switch e {
    case .number(let n):
        if let whole = Int(exactly: n) { return "\(whole)" }
        return "\(n)"
    case .add(let a, let b):
        return "(\(show(a)) + \(show(b)))"
    case .subtract(let a, let b):
        return "(\(show(a)) - \(show(b)))"
    case .multiply(let a, let b):
        return "(\(show(a)) * \(show(b)))"
    case .divide(let a, let b):
        return "(\(show(a)) / \(show(b)))"
    case .negate(let a):
        return "-\(show(a))"
    }
}

// (2 + 3) * -(10 / 4)
let e = Expr.multiply(
    .add(.number(2), .number(3)),
    .negate(.divide(.number(10), .number(4)))
)

print(show(e), "=", evaluate(e))
print(show(.add(.number(1), .multiply(.number(2), .number(3)))))
((2 + 3) * -(10 / 4)) = -12.5
(1 + (2 * 3))

That is an interpreter for arithmetic in forty lines, and the shape generalizes: a tree is an enum with a leaf case and a branch case, a JSON value is an enum with six cases, a linked list is an enum with empty and node. Two functions walk the same tree without either of them knowing about the other, which is what you get when the data is separate from the operations on it.

indirect may be written on a single case or on the whole enum, as here. The cost is one allocation per node, which is what a tree costs in any language.

Exercise 8-3. Add a .power(Expr, Expr) case and let the compiler find the two functions that need it.

Exercise 8-4. Write depth(_:) returning how deeply nested an Expr is.

Exercise 8-5. Write a JSON enum with cases for null, boolean, number, string, array and object, and a function that renders one as text.

8.4 Patterns Outside switch

The patterns a switch arm uses work in several other places, which saves writing a switch with one real arm and a default that does nothing.

// Patterns are not only for switch.
enum Event {
    case click(x: Int, y: Int)
    case key(Character)
    case scroll(Int)
}

let events: [Event] = [
    .click(x: 3, y: 4), .key("a"), .scroll(-2), .key("q"), .click(x: 5, y: 5),
]

// if case tests a single pattern.
if case .click(let x, let y) = events[0] {
    print("first click at \(x),\(y)")
}

// for case keeps only the elements that match.
for case .key(let c) in events {
    print("key", c)
}

// and it composes with where.
for case .click(let x, let y) in events where x == y {
    print("click on the diagonal at \(x),\(y)")
}

// guard case is the early-exit form.
func firstScroll(_ events: [Event]) -> Int? {
    for e in events {
        guard case .scroll(let amount) = e else { continue }
        return amount
    }
    return nil
}
print("first scroll:", firstScroll(events) ?? 0)

// Optional patterns work the same way, because Optional is an enum.
let maybes: [Int?] = [1, nil, 3, nil, 5]
for case let n? in maybes {
    print("value", n)
}
first click at 3,4
key a
key q
click on the diagonal at 5,5
first scroll: -2
value 1
value 3
value 5

for case let n? in maybes is the idiom for walking a collection of optionals and skipping the empty ones. It is compactMap said as a loop, and when the body does real work it reads better.

The ~= operator is what all of this is built on, and you can give it an overload for your own type to make your own values matchable in a case. That is rarely needed and is worth knowing exists.

8.5 Enum or Class Hierarchy

Both model “one of several kinds of thing” and they fail in opposite directions.

An enum fixes the set of cases and makes adding an operation easy: write a new function with a switch and you are done. Adding a case is the expensive move, because every existing switch must be revisited, and the compiler makes you do it.

A class hierarchy fixes the set of operations and makes adding a kind easy: write a new subclass and implement the methods. Adding an operation is the expensive move, because every existing class must grow a method, and nothing makes you do it, so one of them will be missed.

Choose by which axis moves. A set of commands, a set of token types, a set of states: the cases are known and the operations keep coming, so use an enum. A set of plugins, a set of shapes a drawing program can be extended with: the operations are known and the kinds keep coming, so use a protocol, which is Chapter 10.

When both keep moving, neither answer is comfortable, and that discomfort is real rather than a failure to pick correctly.

9. Errors

Swift asks you to sort failures into three kinds and gives each a different mechanism. Getting the sorting right is most of the work; the syntax takes ten minutes.

A value that might not be there is an Optional. There is no error, nothing went wrong, and a dictionary with no entry for a key is behaving correctly. Chapter 5 covered these.

A thing that can fail for reasons outside your control is an error: the file is missing, the network is down, the input is malformed. The caller can reasonably do something about it, so the function throws and the caller decides.

A thing that cannot fail unless the program is wrong is a bug. An index past the end of an array, an arithmetic overflow, a force unwrap of nothing. These stop the program, and they should, because continuing means running code whose assumptions have already been proven false.

Do not use the second mechanism for the third. A function that throws invalidIndex is asking every caller to handle a case that means somebody upstream made a mistake, and none of them can fix it.

9.1 Throwing and Catching

An error is any value whose type conforms to Error, which is an empty protocol. An enum is the usual choice, because a function’s failures are a fixed set of cases with values attached, which is the shape Chapter 8 is about.

// An error is a value of a type you define, thrown and caught by pattern.
enum ConfigError: Error {
    case missingKey(String)
    case notANumber(key: String, value: String)
    case outOfRange(key: String, value: Int, allowed: ClosedRange<Int>)
}

let settings = ["port": "8080", "retries": "many", "timeout": "30"]

func intValue(for key: String, in range: ClosedRange<Int>) throws -> Int {
    guard let text = settings[key] else {
        throw ConfigError.missingKey(key)
    }
    guard let n = Int(text) else {
        throw ConfigError.notANumber(key: key, value: text)
    }
    guard range.contains(n) else {
        throw ConfigError.outOfRange(key: key, value: n, allowed: range)
    }
    return n
}

func report(_ key: String) -> String {
    do {
        return "\(key) = \(try intValue(for: key, in: 1...600))"
    } catch ConfigError.missingKey(let key) {
        return "\(key) is not set"
    } catch ConfigError.notANumber(let key, let value) {
        return "\(key) should be a number, not \"\(value)\""
    } catch ConfigError.outOfRange(let key, let value, let allowed) {
        return "\(key) is \(value), outside \(allowed.lowerBound)...\(allowed.upperBound)"
    } catch {
        return "\(key) failed: \(error)"
    }
}

for key in ["port", "retries", "timeout", "host"] {
    print(report(key))
}

// try? turns a thrown error into nothing, when you do not care which it was.
let port = (try? intValue(for: "port", in: 1...99999)) ?? 80
print("port is \(port)")
port is 8080, outside 1...600
retries should be a number, not "many"
timeout = 30
host is not set
port is 8080

throws goes before the arrow and is part of the function’s type, so it is visible at every call. A call to a throwing function must be marked try, and the compiler insists:

missing_try.swift:7:9: error: call can throw but is not marked with 'try'
7 | let n = risky()
  |         |- note: did you mean to use 'try'?
  |         |- note: did you mean to handle error as optional value?
  |         `- note: did you mean to disable error propagation?

The three notes are the three answers: try and let it propagate, try? and treat failure as nothing, try! and crash if it happens. try is not a block and does not catch anything; it marks the call so a reader scanning the function can see every place control might leave.

catch clauses are patterns, the same patterns as a switch arm, so a thrown enum can be taken apart with its associated values in the catch itself. A bare catch matches anything and binds the error to a name called error without your declaring it. Catches must be exhaustive, which means a bare catch at the end unless the enclosing function itself throws, in which case anything unhandled propagates.

Propagation is the default and the reason this design is worth having. A function that calls three throwing functions and handles none of them writes try three times and says throws once, and the error travels to whoever is in a position to do something about it. Compare a return code, which every layer in between must check and forward by hand, and which any layer may quietly drop.

9.2 defer

defer schedules a block to run when the current scope exits, however it exits: a return, a throw, a break, or falling off the end.

// defer runs on the way out, however the way out happens.
enum Trouble: Error { case badLine(Int) }

func process(_ lines: [String]) -> String {
    print("open")
    defer { print("close") }

    do {
        for (i, line) in lines.enumerated() {
            defer { print("  done with line \(i)") }
            print("  line \(i): \(line)")
            if line.isEmpty {
                throw Trouble.badLine(i)
            }
        }
        return "all \(lines.count) lines"
    } catch {
        return "stopped: \(error)"
    }
}

print(process(["a", "b"]))
print("---")
print(process(["a", "", "c"]))
open
  line 0: a
  done with line 0
  line 1: b
  done with line 1
close
all 2 lines
---
open
  line 0: a
  done with line 0
  line 1: 
  done with line 1
close
stopped: badLine(1)

The cleanup sits next to the thing it cleans up rather than at the bottom of the function in every exit path, which is the point. The second run shows it working on the way out of a throw: the line’s defer still runs, and so does the function’s.

Two rules. Several defers in one scope run in reverse order, last registered first, which is right when the second depends on the first. And the scope is the enclosing braces, not the function, so the defer inside the loop body runs on every turn.

defer is for releasing something you acquired: closing a file, unlocking a lock, restoring a setting. It is not a place to put the second half of a function.

9.3 Result

Result<Success, Failure> is an error stored as a value rather than thrown. It is what you want when the failure has to be carried somewhere before anybody looks at it: across a callback, into an array, out of a batch of operations where each one may fail separately.

// Result is the error as a value, for when you cannot throw at the call.
enum Trouble: Error, CustomStringConvertible {
    case tooShort(String)
    var description: String {
        switch self {
        case .tooShort(let s): return "\"\(s)\" is too short"
        }
    }
}

func check(_ s: String) throws -> String {
    guard s.count >= 3 else { throw Trouble.tooShort(s) }
    return s.uppercased()
}

let outcomes = ["ada", "x", "grace"].map { name in
    Result { try check(name) }
}

for outcome in outcomes {
    switch outcome {
    case .success(let s): print("ok", s)
    case .failure(let e): print("no", e)
    }
}

// A Result can be transformed without being opened.
let lengths = outcomes.map { $0.map(\.count) }
print(lengths.map { (try? $0.get()) ?? -1 })

// Typed throws names the one error a function can produce.
func strict(_ s: String) throws(Trouble) -> String {
    guard s.count >= 3 else { throw .tooShort(s) }
    return s
}

do {
    print(try strict("hi"))
} catch {
    // error is a Trouble here, not any Error
    print("caught a \(type(of: error)): \(error)")
}
ok ADA
no "x" is too short
ok GRACE
[3, -1, 5]
caught a Trouble: "hi" is too short

Result { try something() } runs the closure and captures whichever way it went, which is the bridge from throwing code into stored results. get() is the bridge back: it returns the value or throws the error, so try result.get() puts you back in the throwing world.

Result is an enum with two cases, so it switches like one, and map transforms the success while leaving a failure alone, the way Optional.map does. \.count is a key path, which is a lightweight way of writing { $0.count }.

Conforming the error type to CustomStringConvertible is worth doing whenever a human will read the message. Without it, printing an error gives you the case name and its payload, which is fine for a log and poor in a message to a user.

throws(Trouble) is typed throws: the function declares the one error type it can produce, so the catch gets a Trouble rather than an Error and can switch over its cases exhaustively without a default. Use it where the set of failures is genuinely closed and part of the contract. Plain throws means throws(any Error) and stays the right default for anything that calls into code you do not control.

9.4 Stopping

When the program is wrong, stop it. fatalError("message") always does. precondition(condition, "message") checks a condition that must hold, in release builds as well as debug. assert checks one only in debug builds, for things too expensive to verify in production.

The choice between precondition and assert is about what a wrong answer costs against what the check costs. Validating an index is cheap and belongs in both; verifying that a list of a million items is sorted is not.

A function that can never return is marked -> Never, which tells the compiler that a guard else calling it is a legitimate exit.

Exercise 9-1. Give ConfigError a CustomStringConvertible conformance and delete the pattern-matching catch clauses, keeping only the bare one. Decide which version you would rather maintain.

Exercise 9-2. Write a function that reads a file of numbers and returns [Result<Int, Trouble>], one entry per line, then a second function that turns that into either all the numbers or the first error.

Exercise 9-3. Add a defer to intValue(for:in:) that logs the key on the way out, and confirm it fires on the throwing paths.

Exercise 9-4. Find out what rethrows means and write a function that needs it.

10. Protocols and Generics

A protocol is a list of things a type must provide. A generic is a function or type written once and used with many types. They are the same idea approached from two directions, and together they are how Swift avoids the inheritance hierarchies that other object-oriented languages use for code reuse.

10.1 Protocols

A protocol names requirements: methods, properties with get or get set, initializers, and associated types. A type conforms by providing them, and says so by listing the protocol after a colon.

// A protocol is a contract. An extension on it is a default implementation.
protocol Shape {
    var area: Double { get }
    var perimeter: Double { get }
    var name: String { get }
}

extension Shape {
    var name: String { "\(type(of: self))".lowercased() }

    var summary: String {
        "\(name): area \(rounded(area)), perimeter \(rounded(perimeter))"
    }

    private func rounded(_ x: Double) -> String {
        "\(Double(Int(x * 100)) / 100)"
    }
}

struct Circle: Shape {
    let radius: Double
    var area: Double { 3.14159265 * radius * radius }
    var perimeter: Double { 2 * 3.14159265 * radius }
}

struct Rectangle: Shape {
    let width: Double
    let height: Double
    var area: Double { width * height }
    var perimeter: Double { 2 * (width + height) }
}

struct Square: Shape {
    let side: Double
    var area: Double { side * side }
    var perimeter: Double { 4 * side }
    var name: String { "square (a rectangle really)" }
}

let shapes: [any Shape] = [
    Circle(radius: 1),
    Rectangle(width: 3, height: 4),
    Square(side: 2),
]

for shape in shapes {
    print(shape.summary)
}

print("total area", shapes.reduce(0) { $0 + $1.area })
circle: area 3.14, perimeter 6.28
rectangle: area 12.0, perimeter 14.0
square (a rectangle really): area 4.0, perimeter 8.0
total area 19.14159265

The extension on the protocol is where Swift differs from the interfaces you know. It can supply a default implementation for a requirement, which name uses, so conforming types get one without writing it and Square overrides it. And it can add methods that are not requirements at all, which summary is: every Shape has it, no Shape declares it, and a conforming type cannot replace it in a way that is seen through the protocol.

That distinction catches people. A requirement is dispatched dynamically, so Square’s name is the one you get whether you are holding a Square or a Shape. A method that exists only in the extension is dispatched statically, so a type that defines its own summary gets its version when the static type is Square and the extension’s when the static type is Shape. Put a requirement in the protocol whenever conformers are meant to be able to change it.

Protocols compose. protocol Drawable: Shape inherits requirements, and func draw(_ x: some Shape & Codable) demands both without either knowing about the other. A protocol may be constrained to classes with : AnyObject, which is what lets a conforming reference be weak.

10.2 Protocols the Standard Library Cares About

Conforming to these is how your type joins the language rather than sitting beside it.

Equatable gives ==, Hashable gives use as a dictionary key or set element, Comparable gives < and therefore sorted(), min() and max(). CustomStringConvertible gives a description used by print and by string interpolation. Codable gives conversion to and from JSON and other formats. Sequence gives for-in and a hundred methods with it.

For most of them the compiler writes the conformance:

// The compiler writes Equatable, Hashable and Codable for you.
import Foundation

struct Point: Equatable, Hashable, Codable {
    var x: Int
    var y: Int
}

struct Track: Codable {
    var name: String
    var points: [Point]
    var closed: Bool
}

let track = Track(
    name: "loop",
    points: [Point(x: 0, y: 0), Point(x: 3, y: 4), Point(x: 0, y: 0)],
    closed: true
)

print(Point(x: 1, y: 2) == Point(x: 1, y: 2))
print(Set(track.points).count)

let encoder = JSONEncoder()
encoder.outputFormatting = [.sortedKeys]
let data = try encoder.encode(track)
print(String(decoding: data, as: UTF8.self))

let restored = try JSONDecoder().decode(Track.self, from: data)
print(restored.name, restored.points.count, restored.closed)

// Retroactive conformance: teach a type you do not own a protocol it lacks.
extension Point: Comparable {
    static func < (a: Point, b: Point) -> Bool {
        (a.x * a.x + a.y * a.y) < (b.x * b.x + b.y * b.y)
    }
}
print(track.points.sorted().map(\.x))
true
2
{"closed":true,"name":"loop","points":[{"x":0,"y":0},{"x":3,"y":4},{"x":0,"y":0}]}
loop 3 true
[0, 0, 3]

You write : Equatable, Hashable, Codable and nothing else, provided every stored property already conforms. Track says Codable and gets JSON in both directions for a nested array of structs, with no field list repeated anywhere and therefore nothing to forget to update. Where the JSON field names differ from your property names you add a CodingKeys enum, and that is the only part you usually have to write.

The last lines are retroactive conformance: an extension in your code can make somebody else’s type conform to a protocol, including a protocol you defined. That is how you adapt a type you do not own without wrapping it, and it is the move that protocol-oriented design is built on. Conforming a type you do not own to a protocol you do not own is the one case to avoid, because two libraries doing it differently is a conflict nobody can resolve.

Exercise 10-1. Give Shape a scaled(by:) requirement and implement it for all three types. Then move it to the extension with a default that does nothing sensible, and see which mistakes each version lets you make.

Exercise 10-2. Make Track encode its closed field under the JSON name is_closed without renaming the property.

10.3 Generics

A generic function names one or more type parameters in angle brackets and constrains them to the protocols it needs.

// One function, any element type, with the constraints written down.
func firstDuplicate<T: Hashable>(in items: [T]) -> T? {
    var seen: Set<T> = []
    for item in items {
        if !seen.insert(item).inserted {
            return item
        }
    }
    return nil
}

func largest<C: Collection>(of items: C) -> C.Element? where C.Element: Comparable {
    guard var best = items.first else { return nil }
    for item in items {
        if item > best { best = item }
    }
    return best
}

print(firstDuplicate(in: [3, 1, 4, 1, 5]) ?? -1)
print(firstDuplicate(in: ["a", "b", "c"]) ?? "none")
print(largest(of: [3, 9, 2]) ?? 0)
print(largest(of: "hello") ?? " ")
print(largest(of: [Int]()) ?? -1)

// A generic type. Stack<T> is a real type for each T you use it with.
struct Stack<Element> {
    private var items: [Element] = []

    var isEmpty: Bool { items.isEmpty }
    var count: Int { items.count }

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

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

extension Stack: CustomStringConvertible where Element: CustomStringConvertible {
    var description: String {
        "[" + items.map(\.description).joined(separator: " ") + "]"
    }
}

var s = Stack<Int>()
for n in [1, 2, 3] { s.push(n) }
print(s, s.pop() ?? 0, s)
1
none
9
o
-1
[1 2 3] 3 [1 2]

<T: Hashable> reads “for any type T that is Hashable”. The constraint is not decoration: without it, Set<T> would not compile, because a set needs to hash its elements. Swift checks the body of a generic function once, against the constraints, rather than once per use as C++ templates do, so an error in a generic function is reported where the function is written.

largest constrains a whole collection rather than an array, which is why it works on a String, whose elements are Character and are Comparable. The where clause carries constraints that do not fit in the angle brackets, usually because they are about an associated type of a parameter rather than the parameter itself.

A generic type works the same way, and the conditional extension at the end is worth noticing: Stack is printable when its elements are, and not otherwise. That is how the standard library says an array is Equatable when its elements are, and it lets a type carry a capability it cannot always offer.

Swift’s generics are not erased. Stack<Int> stores its integers directly with no boxing, and the generated code can be specialized per type, so a generic abstraction costs nothing at runtime.

10.4 some and any

Two keywords appear in front of protocol names and they mean opposite things.

// Conform to Sequence and every for-in and every collection method works.
struct Countdown: Sequence, IteratorProtocol {
    var n: Int

    mutating func next() -> Int? {
        guard n > 0 else { return nil }
        defer { n -= 1 }
        return n
    }
}

for i in Countdown(n: 3) {
    print(i, terminator: " ")
}
print()
print(Array(Countdown(n: 6).filter { $0 % 2 == 1 }))
print(Countdown(n: 4).reduce(0, +))

// some names one concrete type without saying which; any boxes several.
protocol Animal {
    func speak() -> String
}

struct Dog: Animal { func speak() -> String { "woof" } }
struct Cat: Animal { func speak() -> String { "meow" } }

func pet() -> some Animal {
    Dog()
}

func pet(number n: Int) -> any Animal {
    n.isMultiple(of: 2) ? Dog() : Cat()
}

print(pet().speak())
print((0..<4).map { pet(number: $0).speak() })

// some works on a parameter too, and there means "any one type, decided by the caller".
func twice(_ animal: some Animal) -> String {
    animal.speak() + " " + animal.speak()
}
print(twice(Cat()))
3 2 1 
[5, 3, 1]
10
woof
["woof", "meow", "woof", "meow"]
meow meow

some Animal is one specific type, fixed at compile time, whose identity the caller is not told. pet() always returns a Dog, and a caller can rely on two calls returning the same type without knowing which, which is what makes it usable in a Set or compared with ==. It costs nothing at runtime.

any Animal is a box that can hold a Dog on one call and a Cat on the next. That flexibility needs indirection and a table of methods, so it costs an allocation and a dynamic call. pet(number:) needs it, because the type really does vary.

The rule: use some unless the type varies at runtime, then use any. An array of mixed conformers, as in [any Shape] earlier, is the ordinary case for any.

Countdown at the top shows how little a protocol conformance can be. Sequence needs one method, next(), returning the next element or nil, and in exchange the type gets for-in, map, filter, reduce, prefix, contains and the rest, all written once in extensions on Sequence. That is protocol extensions doing the work the standard library would otherwise have to repeat for every collection.

10.5 Asking What Is in the Box

A value held as any Shape has a concrete type underneath, and sometimes you need it. is asks, as? converts and hands back nil if the answer is no, and as! converts and crashes if the answer is no.

// Asking what a boxed value actually is.
protocol Shape {}
struct Circle: Shape { let radius: Double }
struct Square: Shape { let side: Double }

let shapes: [any Shape] = [Circle(radius: 1), Square(side: 2), Circle(radius: 3)]

for shape in shapes {
    if let circle = shape as? Circle {
        print("circle of radius \(circle.radius)")
    } else if shape is Square {
        print("some square")
    }
}

// switch does the same thing more neatly when there are several cases.
for shape in shapes {
    switch shape {
    case let c as Circle: print("r=\(c.radius)")
    case let s as Square: print("s=\(s.side)")
    default: print("unknown shape")
    }
}

print(shapes.compactMap { $0 as? Circle }.map(\.radius))
circle of radius 1.0
some square
circle of radius 3.0
r=1.0
s=2.0
r=3.0
[1.0, 3.0]

as? is the one to use, and it pairs with if let exactly as an optional does, because that is what it produces. as! earns the same warning try! and ! get in Chapters 5 and 9: right when the surrounding code has already established the answer, and a crash you chose to write everywhere else.

A switch with as patterns is the readable form when there are more than two possibilities, and it is also a signal. A long chain of casts usually means the protocol is missing a requirement: if every caller has to ask which shape it is holding, the thing they are asking for belongs on Shape itself, and Chapter 8’s closing section is the argument for when to reach for an enum instead.

There is also as with no punctuation, which is not a test at all but a compile-time conversion that is guaranteed to succeed, as in 5 as Double or value as Any.

Exercise 10-3. Write a Fibonacci sequence conforming to Sequence and take its first ten values. Decide what should happen when the numbers overflow.

Exercise 10-4. Change pet() to return any Animal and find out which of the calls in the program stop compiling, and why the some version allowed them.

Exercise 10-5. Add a peek to Stack returning the top without removing it, and a conditional conformance to Equatable.

Exercise 10-6. largest(of:) duplicates the standard library’s max(). Read its declaration and work out what each part of it is doing.

11. Building a Program

Everything so far compiled as a single file. Real programs are packages: several modules, a manifest, dependencies, tests, and a command-line interface. This chapter builds one, a word-frequency counter called wf, and the finished thing is in examples/wf beside this text.

11.1 The Package

swift package init --type executable --name wf

That writes a Package.swift and a source directory. Replace the manifest with this, which splits the program into a library and the command that drives it:

// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "wf",
    targets: [
        .target(name: "WordCount"),
        .executableTarget(name: "wf", dependencies: ["WordCount"]),
        .testTarget(name: "WordCountTests", dependencies: ["WordCount"]),
    ],
    swiftLanguageModes: [.v6]
)

Package.swift is Swift, not a configuration file. It is compiled and run to produce the description, so the manifest can compute things. The swift-tools-version comment on the first line is not a comment: it fixes which manifest API is in use and which language mode the package defaults to.

A target is a module. Its sources live in Sources/<target name> and the directory layout is the configuration, so there is no list of files to keep up to date. A product is what the package offers to others, a library or an executable; a package with no products section, like this one, still builds its executable targets and is simply not meant to be depended on.

The split is the point. WordCount holds the logic and knows nothing about the command line. wf holds the command line and is thin. WordCountTests tests the library, which it can do without running a program or capturing output. Splitting a small program this way costs one line of manifest and is the difference between testable and not.

Dependencies go in two places, the package and the target:

dependencies: [
    .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"),
],
targets: [
    .executableTarget(name: "wf", dependencies: [
        "WordCount",
        .product(name: "ArgumentParser", package: "swift-argument-parser"),
    ]),
]

swift build resolves and fetches them, writing Package.resolved with the exact versions used. Commit that file for an executable, so a build a year from now produces the same program.

11.2 Modules and Access Control

A module is the unit of both namespacing and access control. internal, the default, means visible throughout the module and invisible outside it, which is why every type and method in WordCount that wf needs is marked public.

/// Counts how often each word appears across the lines it is given.
public struct Counter {
    private var counts: [String: Int] = [:]

    public init() {}

    public var distinct: Int { counts.count }
    public var total: Int { counts.values.reduce(0, +) }

    public mutating func add(_ line: String) {
        for word in Counter.words(in: line) {
            counts[word, default: 0] += 1
        }
    }

    /// Words, most frequent first, ties broken alphabetically.
    public func ranked() -> [(word: String, count: Int)] {
        counts
            .sorted { a, b in a.value == b.value ? a.key < b.key : a.value > b.value }
            .map { (word: $0.key, count: $0.value) }
    }

    /// Lowercased runs of letters, with apostrophes kept inside a word.
    static func words(in line: String) -> [String] {
        line.lowercased()
            .split(whereSeparator: { !$0.isLetter && $0 != "'" })
            .map(trimmingApostrophes)
            .filter { !$0.isEmpty }
    }

    private static func trimmingApostrophes(_ word: Substring) -> String {
        var word = word
        while word.first == "'" { word = word.dropFirst() }
        while word.last == "'" { word = word.dropLast() }
        return String(word)
    }
}

Three levels are in use. counts is private, so nothing outside the struct can reach the dictionary and no caller can put it in a state add would not produce. words(in:) is internal by default, so it is invisible to wf and visible to the tests, which is what @testable import is for. Everything else is public, and that list is the module’s surface: four members, each of which somebody outside needs.

A public struct gets no public initializer unless you write one, which is why public init() {} is there. The rule surprises everyone once. A memberwise initializer is internal, because making it public would freeze the property list as part of the module’s contract.

Comments beginning /// are documentation. They are what an editor shows on hover, and what DocC renders into a documentation site once you add the swift-docc-plugin dependency. On a public API they are the difference between a module somebody can use and one they have to read.

11.3 The Command Line

/// The command line, parsed.
public struct Options {
    public var show = 10
    public var path: String?

    public enum Failure: Error, CustomStringConvertible {
        case missingValue(flag: String)
        case notANumber(flag: String, value: String)
        case unknownFlag(String)
        case tooManyFiles

        public var description: String {
            switch self {
            case .missingValue(let flag):
                return "\(flag) needs a value"
            case .notANumber(let flag, let value):
                return "\(flag) wants a number, not \"\(value)\""
            case .unknownFlag(let flag):
                return "unknown option \(flag)"
            case .tooManyFiles:
                return "one file at a time"
            }
        }
    }

    public static let usage = "usage: wf [-n count] [file]"

    /// Parses everything after the program name.
    public init(arguments: [String]) throws {
        var rest = arguments[...]

        while let argument = rest.first, argument.hasPrefix("-"), argument != "-" {
            rest = rest.dropFirst()
            switch argument {
            case "-n":
                guard let value = rest.first else {
                    throw Failure.missingValue(flag: argument)
                }
                rest = rest.dropFirst()
                guard let n = Int(value) else {
                    throw Failure.notANumber(flag: argument, value: value)
                }
                show = n
            default:
                throw Failure.unknownFlag(argument)
            }
        }

        guard rest.count <= 1 else { throw Failure.tooManyFiles }
        path = rest.first
    }
}

The parser lives in the library and takes its arguments as a parameter rather than reading CommandLine.arguments itself, which is what makes it testable without launching a process. That is the same move as putting the counting in Counter: a function that reaches out to the world can only be tested by arranging the world.

arguments[...] takes the whole array as a slice, so consuming an argument is dropFirst() rather than an index that has to be kept right. Chapter 6’s warning about slices keeping their original indices is the reason nothing here indexes with a number.

For anything with more than a few flags, use Apple’s swift-argument-parser, which derives the parsing, the usage text and the completions from a struct. The hand-written version is here because you should see what it is doing once.

import Foundation
import WordCount

func fail(_ message: String) -> Never {
    FileHandle.standardError.write(Data("wf: \(message)\n".utf8))
    exit(2)
}

let options: Options
do {
    options = try Options(arguments: Array(CommandLine.arguments.dropFirst()))
} catch {
    fail("\(error)\n\(Options.usage)")
}

var counter = Counter()

if let path = options.path {
    guard let text = try? String(contentsOfFile: path, encoding: .utf8) else {
        fail("cannot read \(path)")
    }
    for line in text.split(separator: "\n", omittingEmptySubsequences: false) {
        counter.add(String(line))
    }
} else {
    while let line = readLine() {
        counter.add(line)
    }
}

for (word, count) in counter.ranked().prefix(options.show) {
    print(String(format: "%7d  %@", count, word))
}
$ swift run -q wf -n 5 gettysburg.txt
      3  and
      3  nation
      2  a
      2  are
      2  conceived
$ swift run -q wf -n 3 < gettysburg.txt
      3  and
      3  nation
      2  a
$ swift run -q wf -x; echo "exit=$?"
wf: unknown option -x
usage: wf [-n count] [file]
exit=2

A file named main.swift may contain top-level statements, which is what makes this read like a script. The alternative is a type marked @main with a static func main(), which is what you want when the program has real structure or needs async, and Chapter 12 uses it.

CommandLine.arguments includes the program’s own path at position zero, so it gets dropped. Errors go to standard error and the program exits non-zero, so wf composes with other programs in a pipeline: output on stdout, complaints on stderr, and a status a shell can test. A program that prints its errors to stdout has corrupted whatever is downstream of it.

-> Never on fail tells the compiler the function does not come back, which is why the guard else calling it is a legitimate exit and why options is definitely assigned after the do-catch.

Exercise 11-1. Add a -u flag that prints the number of distinct words and the total, instead of the ranking.

Exercise 11-2. Make wf accept several files, printing a heading for each. Decide what tooManyFiles becomes.

Exercise 11-3. Replace the hand-written parser with swift-argument-parser and compare the amount of code, the usage message, and what breaks when you add a flag.

11.4 Tests

The swift-testing library ships with the toolchain. A test is a function marked @Test, and #expect records a failure and keeps going while #require stops the test.

import Testing
@testable import WordCount

@Test func splitsOnPunctuationAndKeepsApostrophes() {
    #expect(Counter.words(in: "Don't stop, don't!") == ["don't", "stop", "don't"])
    #expect(Counter.words(in: "one--two  three") == ["one", "two", "three"])
    #expect(Counter.words(in: "''' ") == [])
}

@Test func countsAndRanks() {
    var c = Counter()
    c.add("the cat sat on the mat")
    c.add("the end")

    #expect(c.total == 8)
    #expect(c.distinct == 6)

    let top = c.ranked()
    #expect(top[0] == (word: "the", count: 3))
    #expect(top[1].word == "cat")
}

@Test func parsesOptions() throws {
    let plain = try Options(arguments: [])
    #expect(plain.show == 10)
    #expect(plain.path == nil)

    let full = try Options(arguments: ["-n", "3", "notes.txt"])
    #expect(full.show == 3)
    #expect(try #require(full.path) == "notes.txt")
}

@Test func rejectsBadOptions() {
    #expect(throws: Options.Failure.self) {
        _ = try Options(arguments: ["-n"])
    }
    #expect(throws: Options.Failure.self) {
        _ = try Options(arguments: ["-n", "many"])
    }
    #expect(throws: Options.Failure.self) {
        _ = try Options(arguments: ["-x"])
    }
}
$ swift test
✔ Test parsesOptions() passed after 0.001 seconds.
✔ Test splitsOnPunctuationAndKeepsApostrophes() passed after 0.001 seconds.
✔ Test countsAndRanks() passed after 0.001 seconds.
✔ Test rejectsBadOptions() passed after 0.001 seconds.
✔ Test run with 4 tests in 0 suites passed after 0.001 seconds.

@testable import raises the module’s internal members to visible for the test target, which is how Counter.words(in:) is tested without being made public. private stays private, so the design is still enforced.

A test function may throw, and a thrown error is a failure, which is why parsesOptions can write try with no do. #expect(throws:) asserts the other direction.

Tests can be grouped in a struct marked @Suite, given names with @Test("counts the words"), run against a list of inputs with @Test(arguments: [...]), and disabled with .disabled("reason"). XCTest still works and is what older code uses; new code has no reason to prefer it.

11.5 Shipping

swift build -c release optimizes, and the binary lands in .build/release/wf. Copy that one file anywhere with a matching Swift runtime, or build with --static-swift-stdlib on Linux to get an executable that carries its own.

swift run builds and runs in one step, -q suppressing the build chatter that would otherwise mix into your program’s output. swift package clean when you suspect the build directory. swift-format formats.

Exercise 11-4. Write a test that fails, run it, and read the output. The failure message shows the values on both sides of the ==, which is the reason #expect is a macro and not a function.

Exercise 11-5. Move Options into its own module and make the tests still pass. Then decide whether the split was worth it.

Exercise 11-6. Add a test that runs the built executable against gettysburg.txt and checks its output, and notice how much more it has to arrange than the tests above.

12. Concurrency

Swift’s concurrency has two halves. One is async/await, which lets a function wait for something without blocking a thread, and reads like ordinary sequential code. The other is a compile-time check that no two pieces of code can touch the same mutable state at the same time. The first half is pleasant. The second half is why Swift 6 exists, and it is the half that will argue with you.

12.1 async and await

A function that waits for something is marked async, and calling one is marked await. The await is a suspension point: the function gives up its thread there and resumes when the answer arrives, and nothing else in your function runs in between.

// async let starts the work now and waits at the first use of the result.
func slowSquare(_ n: Int) async -> Int {
    try? await Task.sleep(for: .milliseconds(100))
    return n * n
}

func sequential() async -> Int {
    var total = 0
    for n in 1...3 {
        total += await slowSquare(n)
    }
    return total
}

func concurrent() async -> Int {
    async let a = slowSquare(1)
    async let b = slowSquare(2)
    async let c = slowSquare(3)
    return await a + b + c
}

let clock = ContinuousClock()

let oneAfterAnother = await clock.measure { _ = await sequential() }
let allAtOnce = await clock.measure { _ = await concurrent() }

print("same answer:", await sequential() == concurrent())
print("sequential waited three times:", oneAfterAnother > .milliseconds(250))
print("concurrent waited once:", allAtOnce < .milliseconds(250))
same answer: true
sequential waited three times: true
concurrent waited once: true

sequential awaits each call before starting the next, so three hundred- millisecond waits take three hundred milliseconds. async let starts the work immediately and the await happens where the value is used, so the three waits overlap and the whole thing takes a hundred.

One await can cover several calls in an expression, which is why await sequential() == concurrent() has a single one. It marks the statement as containing a suspension, not each individual call.

An async function can only be called from another async function, or from a Task, which is the bridge from synchronous code:

Task {
    let answer = await slowSquare(9)
    print(answer)
}

The examples here get away without one because a single file compiled by swiftc is top-level code, and top-level code may await directly. A real program uses @main on a type with a static func main() async, in a file not named main.swift, which is what swift build expects.

12.2 Structured Concurrency

async let handles a fixed number of children known when you write the code. When the number depends on the data, use a task group.

// A task group runs a variable number of children and collects their results.
func measure(_ word: String) async -> (word: String, length: Int) {
    try? await Task.sleep(for: .milliseconds(20))
    return (word, word.count)
}

let words = ["ada", "grace", "alan", "edsger", "barbara"]

var results: [(word: String, length: Int)] = []

await withTaskGroup(of: (word: String, length: Int).self) { group in
    for word in words {
        group.addTask { await measure(word) }
    }
    for await result in group {
        results.append(result)
    }
}

// Results arrive in whatever order they finish, so sort before printing.
for result in results.sorted(by: { $0.word < $1.word }) {
    print(result.word, result.length)
}
print("children:", results.count)
ada 3
alan 4
barbara 7
edsger 6
grace 5
children: 5

Both forms are structured: the children cannot outlive the scope that started them. withTaskGroup does not return until every child has finished, and a function with async let awaits or cancels its children before it returns. That is what makes the lifetimes something you can reason about by reading, rather than something you track with handles and hope.

Cancellation travels down the same tree. Canceling a parent cancels its children, and cancellation is cooperative: a task is asked to stop and must notice, by checking Task.isCancelled or by calling something like Task.checkCancellation() that throws. Work that never checks never stops.

for await walks an AsyncSequence, which is the asynchronous cousin of Chapter 10’s Sequence: a group is one, and so are the lines of a file and the values of a stream.

An unstructured Task { } is the escape hatch, for work that genuinely must outlive its caller. It gives you a handle you can await or cancel, and it is your responsibility rather than the scope’s. Prefer the structured forms.

12.3 Actors

An actor is a reference type that protects its own mutable state. Only one task executes inside an actor at a time, so its properties cannot be read and written simultaneously from two places.

// An actor owns its state. Only one task is inside it at a time.
actor Tally {
    private var counts: [String: Int] = [:]

    func add(_ key: String) {
        counts[key, default: 0] += 1
    }

    func snapshot() -> [String: Int] {
        counts
    }
}

let tally = Tally()

await withTaskGroup(of: Void.self) { group in
    for i in 0..<1000 {
        group.addTask {
            await tally.add(i.isMultiple(of: 2) ? "even" : "odd")
        }
    }
}

let final = await tally.snapshot()
print("even", final["even"] ?? 0)
print("odd ", final["odd"] ?? 0)
print("total", final.values.reduce(0, +))
even 500
odd  500
total 1000

A thousand tasks increment a shared dictionary and the answer is right, with no lock written anywhere. Calls from outside the actor are awaited, because they may have to wait for whoever is inside; calls between an actor’s own methods are not.

@MainActor is a global actor, a single actor that a whole set of declarations belongs to. Marking a type or a function @MainActor means it runs on the main thread, which is what user-interface code requires. In Swift 6 the top level of a program is @MainActor by default, which is why a global var in a script cannot be touched from a detached task without saying more.

The rule that catches people: an actor protects its state, not the objects its state refers to. Handing out a reference to a mutable class from inside an actor hands out the race you were avoiding. Keep actor state in value types and this does not arise.

12.4 Sendable

A value that crosses from one concurrency domain to another must be safe to send, which is what Sendable means. Value types made of Sendable parts are Sendable automatically, as are actors and immutable classes. A mutable class is not, and Swift 6 refuses to let one cross:

data_race.swift:10:13: error: non-Sendable type 'Counter' of let 'counter' cannot exit main actor-isolated context [#NonSendableExitingActor]
 1 | final class Counter {
   |             `- note: class 'Counter' does not conform to the 'Sendable' protocol
 9 |         group.addTask {
10 |             counter.value += 1

[#NonSendableExitingActor]: <https://docs.swift.org/compiler/documentation/diagnostics/getting-non-sendable-values-out-of-actors>

In Swift 5 that program compiles and produces a number slightly under a hundred, different on each run, for reasons that take an afternoon to find. In Swift 6 it does not compile. That is the trade the language is making, and it is the reason strict concurrency was worth a major version.

The fixes, in the order to try them. Make the type a struct, so each task gets its own copy and there is nothing to share. Make it an actor, so the sharing is mediated. Make the class immutable — let properties all the way down — and mark it Sendable. Only when none of those fit, mark it @unchecked Sendable and take responsibility for the locking yourself, which is a promise to the compiler that you have done what it could not verify.

This is where Chapter 7 pays off. A program built out of values has very little that needs to be Sendable in the first place, because there is nothing shared to race over. A program built out of mutable objects has to solve this problem everywhere at once, and that is what porting an old codebase to Swift 6 feels like.

Exercise 12-1. Rewrite taskgroup.swift with async let and find out why you cannot, without changing the shape of the input.

Exercise 12-2. Make measure throw for an empty word, switch to withThrowingTaskGroup, and decide what should happen to the other children when one fails.

Exercise 12-3. Replace the actor in actors.swift with a final class and read the error. Then fix it three ways: a struct, an actor, and @unchecked Sendable with a lock.

Exercise 12-4. Start a Task that loops forever, cancel it after a moment, and make it actually stop.

12.5 What This Book Left Out

Property wrappers and result builders, which is how @State and SwiftUI’s view syntax are built. Macros, which is what #expect is. Unsafe pointers and manual memory, which you need for talking to C. Objective-C interoperation. The whole of SwiftUI, UIKit, and the Apple frameworks, which are a larger subject than the language and are documented by the people who wrote them.

What you have is the language: its type system, its two kinds of type and the reason for choosing between them, the absence of null, the exhaustive switch, protocols and generics, and a concurrency model that checks its own promises. Everything above is written in terms of those, so you now have the vocabulary to read it.

The next thing to write is a program you want. Pick one that reads something and prints something, keep the logic in a module and the world at the edges, and let the compiler argue with you. It is usually right.

Appendix A: Reference Summary

A condensed reminder of the syntax, in the order the book introduced it. It is for looking something up, not for learning it.

Declarations

let name = value                  // constant
var name = value                  // variable
let name: Type = value            // with the type written out
typealias Name = Type             // a second name for a type

Names are lowerCamelCase for values and functions, UpperCamelCase for types. A keyword may be used as a name by wrapping it in backticks.

Types

Int and the sized Int8 … Int64, UInt8 … UInt64. Double, Float. Bool. Character, String. [Element], [Key: Value], Set<Element>. (A, B) tuple, (A) -> B function, T? optional.

Conversion is an initializer: Double(n), Int(x) truncating, Int(exactly: x) returning nil when it will not fit, UInt8(truncatingIfNeeded: n) keeping the low bits.

Literals: 1_000_000, 0xff, 0o17, 0b1010, 1.6e-19, 0x1p-3.

Operators

Arithmetic + - * / %, wrapping &+ &- &*. Comparison == != < <= > >=, identity ===. Logical && || !, short-circuiting. Nil-coalescing ??. Conditional c ? a : b. Ranges a..<b, a...b, a..., ...b. Assignment produces no value.

Control Flow

if condition { } else if condition { } else { }
let x = if c { a } else { b }          // as an expression

guard condition else { return }        // else must leave
guard let x = optional else { return } // x stays in scope after

while condition { }
repeat { } while condition

for x in sequence { }
for x in sequence where condition { }
for (i, x) in sequence.enumerated() { }
for case .some(let x) in sequence { }

break, continue, break label, continue label
label: for ... { }
switch value {
case pattern:
case a, b:                    // alternatives
case 0..<10:                  // range
case let (x, y) where x > y:  // binding and filter
default:
}

Cases do not fall through; fallthrough if you want C’s behavior. A switch must be exhaustive, and no arm may be empty.

Functions and Closures

func name(label internal: Type = default, _ unlabeled: Type) -> Type { }
func name(_ items: Type...) -> Type { }          // variadic
func name(_ x: inout Type) { }                   // caller writes &x
func name() -> Never { }                         // does not return

A single-expression body needs no return. The full name includes the labels: name(label:_:).

{ (a: Type, b: Type) -> Type in body }
{ a, b in expression }
{ $0 < $1 }
f(x) { $0 < $1 }                 // trailing closure
{ [weak self, copied] in body }  // capture list
@escaping                        // parameter attribute, closure outlives the call

Optionals

var x: T?                       // Optional<T>
if let x { }                    // shorthand when the name is reused
if let y = x, let z = w, cond { }
guard let x else { }
while let x = f() { }
x ?? fallback
x?.member                       // whole chain is nil if any link is
x!                              // crash if nil
switch x { case .some(let v): ; case nil: }
x.map { }, x.flatMap { }

Types You Define

struct Name {
    let a: Int                  // stored
    var b: Int = 0
    var c: Int { a + b }        // computed, read-only
    var d: Int { get { } set { } }
    var e: Int = 0 { willSet { } didSet { } }
    static let shared = ...

    init(a: Int) { self.a = a }
    mutating func change() { }
}

final class Name: Super, Protocol {
    init() { super.init() }
    convenience init(x: Int) { self.init() }
    deinit { }
    override func method() { super.method() }
}

enum Name: RawType, CaseIterable {
    case simple
    case withValues(a: Int, b: String)
    indirect case recursive(Name)
}

protocol Name: AnyObject {
    var required: Int { get set }
    func method() -> Int
    init(x: Int)
    associatedtype Element
}

extension Existing: Protocol where Element: Comparable { }

weak var and unowned let do not raise the reference count; weak is optional and becomes nil.

Access, widest first: open, public, package, internal (the default), fileprivate, private. private(set) narrows only writing.

Generics

func f<T: Protocol>(_ x: T) -> T { }
func f<C: Collection>(_ x: C) -> C.Element? where C.Element: Comparable { }
struct Box<Element> { }
extension Box: Equatable where Element: Equatable { }

func f(_ x: some Protocol)      // one type, chosen by the caller
func f() -> some Protocol       // one type, hidden from the caller
let xs: [any Protocol]          // several types, boxed

Protocols the standard library uses: Equatable, Hashable, Comparable, CustomStringConvertible, Codable, Sequence, IteratorProtocol, Collection, Error, Sendable. Equatable, Hashable and Codable are synthesized when every stored property conforms.

Errors

enum MyError: Error { case bad(String) }

func f() throws -> Int { throw MyError.bad("why") }
func f() throws(MyError) -> Int { }        // typed throws
func f(_ body: () throws -> Void) rethrows { }

do {
    let x = try f()
} catch MyError.bad(let why) {
} catch let e as MyError {
} catch {                                   // binds `error`
}

try? f()        // -> Optional, nil on failure
try! f()        // crash on failure
defer { }       // runs on the way out, reverse order

Result<Success, Failure>, .success, .failure, get(), Result { try f() }

fatalError(), precondition(_:_:), assert(_:_:)

Concurrency

func f() async -> Int { }
let x = await f()

async let a = f()                    // starts now, awaited at first use
await withTaskGroup(of: T.self) { group in
    group.addTask { await f() }
    for await r in group { }
}
Task { }                             // unstructured; has cancel()
Task.isCancelled, try Task.checkCancellation()
try await Task.sleep(for: .milliseconds(100))

actor Name { }                       // one task inside at a time
@MainActor                           // the main-thread global actor
Sendable, @unchecked Sendable

Packages

// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "tool",
    targets: [
        .target(name: "Library"),
        .executableTarget(name: "tool", dependencies: ["Library"]),
        .testTarget(name: "LibraryTests", dependencies: ["Library"]),
    ],
    swiftLanguageModes: [.v6]
)

swift build, swift build -c release, swift run, swift test, swift package clean, swift-format.

Sources live in Sources/<target>; tests in Tests/<target>. A file named main.swift may hold top-level code; otherwise a type marked @main with static func main() is the entry point.

import Testing
@testable import Library

@Test func name() throws {
    #expect(a == b)
    let x = try #require(optional)
    #expect(throws: MyError.self) { try f() }
}

Appendix B: Coming From Another Language

Three translation tables, each followed by the traps that table does not show. Read the one you arrived from.

From Lux

Lux was built as the centroid of Rust, Swift and Go, so the ideas line up directly and mostly what changes is spelling. lux convert swift will translate a program you already understand, and reading its output beside your original is the fastest version of this appendix.

LuxSwift
let x = 1 / var x = 1same
int, float, string, boolInt, Double, String, Bool
func f(n: int) -> int { }func f(n: Int) -> Int { }
f(n: 3)f(n: 3)
print(a, b)print(a, b)
"Score: " + string(42)"Score: \(42)"
length(s) / length(a)s.count / a.count
[int], primes[0][Int], primes[0]
queue += 4queue.append(4)
struct Point { x: int }struct Point { var x: Int }
Point(x: 3, y: 4)Point(x: 3, y: 4)
enum Shape { circle(radius: float) }enum Shape { case circle(radius: Double) }
Shape.circle(radius: 2.0).circle(radius: 2.0)
match s { circle(let r) => ... }switch s { case .circle(let r): ... }
_ =>default:
Option<int>, some(x), noneInt?, x, nil
match o { some(let x) => ...; none => ... }if let x = o { ... } else { ... }
Result<int, string>, ok(x), err(e)throws -> Int, return x, throw e
for x in 0..nfor x in 0..<n
if, while, returnsame

Four things will surprise you.

Strings interpolate rather than concatenate. "a" + string(n) is legal Swift only after the conversion, and nobody writes it; "a\(n)" is the idiom, and it takes any expression.

Errors are thrown rather than returned. Lux’s Result is a value you match, and Swift has Result too, but the normal way to write a fallible function is throws, with the error traveling up on its own until somebody catches it. Chapter 9 covers both and says when each is right. Lux’s rule that a Result may not be stored has no counterpart here, because Swift’s Result is an ordinary value.

Swift has classes, and Lux deliberately does not. Chapter 7 is the chapter with no Lux equivalent, and its argument is that you should keep using struct, which is the only thing Lux let you do, until you meet a reason not to.

Swift’s switch on an enum case needs the leading dot and the case keyword, and arms end at the next case rather than falling through. The exhaustiveness rule is the one you already know, and it is enforced the same way.

From Python

PythonSwift
x = 1var x = 1, or let x = 1 if it never changes
def f(n): return n * 2func f(_ n: Int) -> Int { n * 2 }
f(3)f(3), but labels appear unless declared _
f"{name} is {age}""\(name) is \(age)"
len(x)x.count
[1, 2, 3][1, 2, 3]
{"a": 1}["a": 1]
{1, 2}Set([1, 2])
for x in xs:for x in xs {
for i, x in enumerate(xs):for (i, x) in xs.enumerated() {
if x:if x != 0, if !x.isEmpty, if x != nil
Nonenil, and the type must be T?
x if c else yc ? x : y
[f(x) for x in xs if p(x)]xs.filter(p).map(f)
try: / except E as e:do { / catch let e as E {
raise E()throw E()
with open(p) as f:defer { }, or a closure-taking API
class C:struct C first; class C when identity matters
d.get(k, 0)d[k] ?? 0
d[k] = d.get(k, 0) + 1d[k, default: 0] += 1

The three that will cost you an afternoon each.

Everything has a type and the type is fixed. A variable that holds an Int holds an Int forever, a list holds one kind of thing, and a function’s return type is the same on every path. What feels like a restriction in the first hour is the thing that removes the test you would otherwise have to write.

There is no truthiness and no None hiding in a variable of ordinary type. A value that might be missing has a different type, T?, and the compiler makes you handle the empty case at the point where it might be empty. Chapter 5 is the one to read twice.

Assignment copies. b = a for a list, a dictionary, or an instance of a struct gives you a second one, and changing b leaves a alone. This is the opposite of Python and it removes the aliasing bugs that copy.deepcopy exists to paper over. Chapter 7 explains why it is affordable.

From JavaScript

JavaScriptSwift
let x = 1 / const x = 1var x = 1 / let x = 1
function f(n) { return n * 2 }func f(_ n: Int) -> Int { n * 2 }
(n) => n * 2{ n in n * 2 } or { $0 * 2 }
`${name} is ${age}`"\(name) is \(age)"
xs.lengthxs.count
xs.map(f), .filter, .reducesame names, same order
xs.push(v)xs.append(v)
{a: 1} as a bag of values["a": 1]
{a: 1} as a recordstruct with fields
for (const x of xs)for x in xs
null and undefinednil, once, and the type says so
x ?? yx ?? y
a?.b?.ca?.b?.c
=====, which compares values; === compares object identity
try / catch (e)do / catch , and calls are marked try
async function / awaitfunc ... async / await
Promise.all([a, b])async let a = ...; async let b = ...
JSON.stringify / JSON.parseJSONEncoder / JSONDecoder with Codable
class C {}struct C first; class C when identity matters

The traps are the same three as Python’s, plus one of your own.

== in Swift is not === in JavaScript. It compares contents, so two separately built structs with equal fields are equal, and there is no coercion to worry about because there is no coercion anywhere. When you want to ask whether two class references point at the same object, === is the operator, and it does not apply to structs at all.

Numbers are not all the same type. JavaScript has one number; Swift has Int and Double and will not mix them without being told. Division of two Ints truncates, which is the first thing that will surprise you and is covered in Chapter 2.

The Shape That Does Not Translate

Every table above stops at the same place. Swift asks you to decide, for each type you define, whether its values are copied or shared, and neither Lux nor Python nor JavaScript asks that question. It is not syntax and there is no row for it.

Chapter 7 is that chapter. Read it slowly, write both versions of something small, and watch what changes. Everything else in this book is vocabulary you already had.

Appendix C: Reading Compiler Errors

Swift’s diagnostics are good and the notes attached to them are often the literal fix, so the habit worth building is reading all of a message rather than the first line. Every message quoted here came from compiling the file of that name in examples/errors, and tools/check.sh confirms each still says what it says.

The shape is always the same. A location, a severity, a message. Then the source around it, with the relevant span marked. Then zero or more note lines, which either explain where the other half of the problem is or tell you what to type.

Some messages end with a name in brackets, such as [#NonSendableExitingActor], and a matching line at the bottom giving a URL. That is the diagnostic’s group, and the page it links to is a written explanation of the whole class of mistake rather than this one instance of it. The concurrency errors are where you will want it most, and they are the ones that have it.

let and var

assign_let.swift:2:1: error: cannot assign to value: 'fahr' is a 'let' constant
1 | let fahr = 0
  | `- note: change 'let' to 'var' to make it mutable
2 | fahr = fahr + 20
  | `- error: cannot assign to value: 'fahr' is a 'let' constant

The note points at the declaration, not the assignment, which is the pattern to expect: Swift shows you both ends of a contradiction. The fix is the note, unless the real answer is that you meant to make a new value instead.

Types do not mix

mixed_numbers.swift:3:17: error: binary operator '+' cannot be applied to operands of type 'Double' and 'Int'
3 | let next = fahr + step
  |                 |- error: binary operator '+' cannot be applied to operands of type 'Double' and 'Int'
  |                 `- note: overloads for '+' exist with these partially matching parameter lists: (Double, Double), (Int, Int)

The note lists what would have worked, which tells you that the operator is not missing, only that neither side will move on its own. Convert one: fahr + Double(step).

wrong_type.swift:1:20: error: cannot convert value of type 'Int' to specified type 'String'
1 | let name: String = 5
  |                    `- error: cannot convert value of type 'Int' to specified type 'String'

When this appears far from where you think the problem is, the annotation you wrote is usually fighting an inference Swift made several lines earlier. Deleting the annotation and printing type(of: x) is the fastest way to find out what it decided.

Nothing is truthy

int_as_bool.swift:2:4: error: type 'Int' cannot be used as a boolean; test for '!= 0' instead
1 | let count = 3
2 | if count {
  |    `- error: type 'Int' cannot be used as a boolean; test for '!= 0' instead

The suggested fix is rarely what you want. if !items.isEmpty is what the code meant.

Optionals

unwrapped_line.swift:2:7: error: value of optional type 'String?' must be unwrapped to refer to member 'count' of wrapped base type 'String'
1 | let line = readLine()
2 | print(line.count)
  |       |- note: chain the optional using '?' to access member 'count' only for non-'nil' base values
  |       `- note: force-unwrap using '!' to abort execution if the optional value contains 'nil'
optional_math.swift:3:7: error: value of optional type 'Int?' must be unwrapped to a value of type 'Int'
3 | print(a + b)
  |       |- note: coalesce using '??' to provide a default when the optional value contains 'nil'
  |       `- note: force-unwrap using '!' to abort execution if the optional value contains 'nil'

The notes offer ?, ?? and !, and they are listed in the order you should prefer them. The option they do not offer is the one that is usually right: if let or guard let, which handles the empty case with code rather than with a default or a crash.

switch must be exhaustive

non_exhaustive.swift:6:5: error: switch must be exhaustive
 5 | func turn(_ d: Direction) -> String {
 6 |     switch d {
   |     |- error: switch must be exhaustive
   |     `- note: add missing case: '.west'

The note names the missing case. Resist adding a default to silence it: the default is what stops this message appearing the next time somebody adds a case, and this message is the feature.

Structs and mutation

missing_mutating.swift:3:21: error: left side of mutating operator isn't mutable: 'self' is immutable
3 |     func bump() { n += 1 }
  |     |               `- error: left side of mutating operator isn't mutable: 'self' is immutable
  |     `- note: mark method 'mutating' to make 'self' mutable
mutating_let.swift:7:9: error: cannot use mutating member on immutable value: 'counter' is a 'let' constant
6 | let counter = Counter()
  | `- note: change 'let' to 'var' to make it mutable
7 | counter.bump()

The second is the first one’s consequence at the call site. If the note’s advice feels wrong, it may be: a type whose methods all want to be mutating is sometimes a type that should be a class, and more often a type whose methods should return a new value.

Strings are not arrays of bytes

string_int_index.swift:2:7: error: 'subscript(_:)' is unavailable: cannot subscript String with an Int, use a String.Index instead.
1 | let name = "Ada Lovelace"
2 | print(name[0])

The unavailable marker means the standard library deliberately declared this so it could give you a message rather than a “no such member”. name.first, name.prefix(n), and name[name.index(name.startIndex, offsetBy: n)] are the three things people usually want.

Errors

missing_try.swift:7:9: error: call can throw but is not marked with 'try'
7 | let n = risky()
  |         |- note: did you mean to use 'try'?
  |         |- note: did you mean to handle error as optional value?
  |         `- note: did you mean to disable error propagation?

Three notes, three answers: try, try?, try!. The first is the default and requires the enclosing function to throw or to have a do-catch.

Closures

escaping.swift:2:21: error: converting non-escaping parameter 'handler' to generic parameter 'Element' may allow it to escape
1 | func register(_ handler: () -> Void, into handlers: inout [() -> Void]) {
2 |     handlers.append(handler)
  |                     `- error: converting non-escaping parameter 'handler' to generic parameter 'Element' may allow it to escape

“May allow it to escape” means the closure is being stored somewhere that outlives the call. Mark the parameter @escaping.

Concurrency

data_race.swift:10:13: error: non-Sendable type 'Counter' of let 'counter' cannot exit main actor-isolated context [#NonSendableExitingActor]
 1 | final class Counter {
   |             `- note: class 'Counter' does not conform to the 'Sendable' protocol
 9 |         group.addTask {
10 |             counter.value += 1

[#NonSendableExitingActor]: <https://docs.swift.org/compiler/documentation/diagnostics/getting-non-sendable-values-out-of-actors>

This is the message that will take the longest to stop resenting. It is saying that a mutable class is about to be touched from two places at once. The note tells you what the type lacks, the linked page explains the class of mistake, and Chapter 12 lists the four fixes in the order to try them.

When the message is useless

Occasionally you get something vague, most often from a long expression with several literals and a generic call in it. The compiler has failed to find a solution and cannot tell you which part is impossible.

The reliable move is to break the expression up. Assign the pieces to named constants with explicit types, one line each, and the error attaches itself to the line that is actually wrong. This costs thirty seconds and beats staring at it. Once it compiles, put it back together if you still want to.