October 4, 2017·4 min read·engineeringswiftcollectionsapi

Swift dictionaries make duplicate-key policy explicit

Swift 4's dictionary APIs distinguish unique keys from deliberate merging. Grouping and value transformations preserve more information about the operation than rebuilding a dictionary manually.

Building a dictionary from pairs includes a decision about duplicate keys. A duplicate might mean invalid input, a replacement value or another contribution to an accumulated total. Swift's revised dictionary APIs make that policy part of the operation's signature. The caller can assert uniqueness or provide the function that resolves collisions.

The October 4 explanation develops these APIs through grouping, counting and merging examples. The changes shipped with the Swift 4 release. Most are also available in Swift 3.2 compatibility mode, with the custom keys and values collections as the stated exception. An application can therefore use many of them before completing its source migration.

I like Swift's value collections because local mutation is usually easier to reason about than shared mutable state. These APIs improve the description of that mutation. A merge that sums counts communicates a different operation from one that keeps the newest value, even though both produce a dictionary.

A uniqueness assertion can fail

Dictionary(uniqueKeysWithValues:) builds a dictionary from key-value pairs when the keys are known to be distinct. Duplicate keys cause a runtime error. The initializer's name describes a requirement on the input, so using it is a claim about the data being supplied.

When repeated keys are possible, Dictionary(_:uniquingKeysWith:) accepts a closure receiving the existing and incoming values. The closure returns the value to keep. This example treats repeated entries as contributions to a count.

let entries = [("queued", 2), ("running", 1), ("queued", 3)]
let counts = Dictionary(entries, uniquingKeysWith: +)
// counts["queued"] == 5

Addition is appropriate because these values represent quantities being combined. A replacement policy could instead return the incoming value. Keeping the existing value would express a different rule again. The API does not guess which policy fits the domain, and the call site records the chosen one.

That explicit choice is useful when reviewing input handling. A dictionary silently constructed with a replacement rule can hide duplicate records. An initializer that requires uniqueness can expose invalid input, while a merging initializer can express intentional aggregation. The right operation depends on what duplicate keys mean for the particular data.

Grouping preserves every input element

Dictionary(grouping:by:) solves another common operation without discarding repeated keys. A closure computes a key for each element, and the resulting dictionary maps each key to an array of its elements. The elements within each group retain their order from the original sequence.

let names = ["api", "worker", "auth"]
let byLength = Dictionary(grouping: names, by: { $0.count })
// byLength[3] == ["api"]
// byLength[4] == ["auth"]

The grouping initializer differs from a collision policy that selects one value. It collects the original elements, so later code can inspect a group or transform it. The order guarantee applies within those arrays. It should not be confused with a promise about the iteration order of dictionary keys.

mapValues can then transform the arrays into counts while preserving the keys. The announcement explains an implementation advantage. Because the key set remains unchanged, the result can reuse the original dictionary's internal layout and avoid recomputing hash values. This is a more specific operation than mapping arbitrary pairs and constructing a dictionary again.

The advantage follows from the restriction. A transformation that changes keys needs another operation and another duplicate-key decision. mapValues can preserve the existing layout because it promises to leave that part of the dictionary alone. The API expresses enough information for the implementation to avoid unnecessary work.

Defaults support accumulation through a subscript

The ordinary dictionary subscript returns an optional because a key might be absent. The new defaulted subscript supplies a value for that case. It can also participate in mutation, which makes a counting loop direct.

var frequencies: [String: Int] = [:]
for name in ["api", "worker", "api"] {
    frequencies[name, default: 0] += 1
}
// frequencies["api"] == 2

For an existing key, the operation retrieves and increments its count. For an absent key, it begins with zero and stores the incremented value under that key. The announcement explains the same read-modify-write behavior with a shopping cart. It removes the repeated lookup and insertion branching from an ordinary accumulation loop.

I value this form because the default belongs to the operation that needs it. Zero is the starting count for this accumulation, and the subscript keeps it next to the increment. A different domain may need a different starting value, so the API takes that value explicitly.

Collection-specific operations retain their result type

Dictionary filtering now produces a dictionary instead of an array of pairs. Set filtering similarly returns a set. A selection operation can therefore preserve the collection abstraction the next piece of code expects without an additional reconstruction step.

Bulk updates have corresponding mutating and nonmutating forms. merge(_:uniquingKeysWith:) changes the receiving dictionary. merging(_:uniquingKeysWith:) produces a new dictionary. Both require the same explicit treatment of repeated keys, so choosing whether to mutate does not remove the collision-policy decision.

The release also provides capacity inspection and reserveCapacity for dictionaries and sets. These controls address storage allocation separately from the meaning of a transformation. Together with the custom key and value collections, they make common collection operations more specific. Code can state that it is preserving keys, grouping every element or combining duplicate values, and the implementation can use those constraints when constructing the result.