Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.