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.
| Lux | Swift |
|---|---|
let x = 1 / var x = 1 | same |
int, float, string, bool | Int, 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 += 4 | queue.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), none | Int?, 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..n | for x in 0..<n |
if, while, return | same |
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
| Python | Swift |
|---|---|
x = 1 | var x = 1, or let x = 1 if it never changes |
def f(n): return n * 2 | func 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 |
None | nil, and the type must be T? |
x if c else y | c ? 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) + 1 | d[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
| JavaScript | Swift |
|---|---|
let x = 1 / const x = 1 | var 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.length | xs.count |
xs.map(f), .filter, .reduce | same names, same order |
xs.push(v) | xs.append(v) |
{a: 1} as a bag of values | ["a": 1] |
{a: 1} as a record | struct with fields |
for (const x of xs) | for x in xs |
null and undefined | nil, once, and the type says so |
x ?? y | x ?? y |
a?.b?.c | a?.b?.c |
=== | ==, which compares values; === compares object identity |
try / catch (e) | do / catch , and calls are marked try |
async function / await | func ... async / await |
Promise.all([a, b]) | async let a = ...; async let b = ... |
JSON.stringify / JSON.parse | JSONEncoder / 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.