Introduction
This book is the product of our shared belief that programs and programming can be better. Over the years we’ve seen philosophies about how to do software engineering come and go; we’ve listened to the experts and applied their tips (sometimes to our detriment!) Looking back, though, the key ideas underlying our best work weren’t the popular ones, and aren’t widely known or taught today. We wrote this book because we think they could make a difference for you, too.
The book’s title comes from Sean’s Better Code series of talks. The first of these, subtitled “Three Goals for Better Code” was given over a decade ago, after Herb Sutter suggested Sean lay out practical tips a developer could apply immediately to improve their code. So this is above all a practical book. The authors each have more than 40 years’ experience writing code that powers real applications in all sorts of contexts; the ideas you’ll encounter here are the ones that stood the test of time and delivered results at scale. We think you’ll also discover, as we have, that the most impactful practical advice is also deeply principled and satisfying—both aesthetically and, in some sense, scientifically.
Better code has more of three ingredients: correctness, efficiency, and abstraction. Most people have a good intuitive sense of what correctness and efficiency are, though we will try to deepen and solidify all three ideas throughout the book. Abstraction is less commonly understood, so, briefly, it’s the quality of code that is more reusable—and paradoxically, more understandable—because inessential detail has been “lifted” out. It’s what many people would call “higher-level code.”
Code usually doesn’t get better all at once. Often we make incremental improvement along one of the three axes as we discover the nature of the problem we’re solving.
We’ve tried to structure the book so chapters build on one another, but they are interdependent. A later chapter may deepen an earlier one, so we encourage you to circle back and re-read earlier material. Each chapter begins with a rubric such as “no raw loops.” The rubrics are often phrased in the negative, to call out red flags, and are intended to be aspirational touchstones, not hard rules. That said, we’ve found they are most powerful when we violate them only as an absolute last resort, and we suggest you do the same.
Although the ideas you’ll find here are applicable to any programming language, we’ve chosen to write our examples in Swift, for its direct, safe support of mutable value semantics (see chapter XX), and generic programming (see chapter XX), and because we are able to express them without language-specific wrinkles that would need to be explained, lengthening the text and distracting from the real issues. If you don’t already know Swift, we’ve provided a brief introduction in appendix YY.
Out of pursuing better code, you should find you have greater confidence in your work and that you produce quality results more quickly, with less code. Programs become easier to maintain, and—when a bug inevitably occurs—to debug. At a metaphysical level, Better Code is a journey of discovery. For us it has been an exhilarating one. Wherever you are on that road, we hope you have the same experience, and that this book serves as a guide and an ongoing companion.
Contracts
No informal agreements—get it in writing.
Software development at scale is fundamentally about managing complexity and establishing trust between components. When you call a function, use a library, or interact with an API, you’re entering into an implicit agreement—but what exactly are the terms of that agreement? Without clear, documented expectations, even the most carefully crafted code becomes a source of confusion, bugs, and maintenance nightmares.
This chapter introduces programming by contract, an approach that transforms how you think about, design, and implement software components. Contracts are more than just comments or documentation—they’re the formal agreements that define how different parts of your system communicate and collaborate.
Contracts are the connective tissue of healthy software. You really can’t build software at scale without them. If you are successfully building large software systems, you’re already using contracts in some form, and this chapter should help you make them more powerful.
Writing contracts isn’t just about preventing bugs—it’s about creating a development process where better design emerges naturally, testing becomes focused, and documentation actually improves your code. These practices will make your software construction both more rigorous and more efficient.
In pursuit of Correctness
Fundamentally, contracts are about the first ingredient of Better Code: correctness. It’s popular in some circles to scoff at the idea of correct software, on the grounds that bugs are inevitable, and because for the most part, we rely on incorrect software every day without disaster. But there is a practical approach to correctness that improves both code quality and the programming experience, without demanding unrealistic perfection.
For those who like to think formally, we can define a semi-correct program as one for which there exists a valid initial state and inputs, such that the program will generate a valid output. As an analogy, a broken clock is semi-correct, it is correct twice a day. This definition provides a partial ordering of correctness, where a program is more correct than another if the initial states under which it is correct are a superset of the other.
So while you may not reach a point where all bugs vanish, you will have well-justified confidence in the correctness of your own work while maintaining high productivity. Also, the discipline we’ll describe here of programming by contract removes uncertainty and needless complexity from your code and from the process of coding. That’s a big part of why we say it’s practical.
Reasoning about correctness
How can you know whether your program is correct? Validating
correctness seldom requires a formal proof—usually the everyday
thinking we do while programming works fine.
var names = [ "Sean", "Laura", "Dave", "Crusty" ]
names.sort()
print(names[3])
In the example above, how do we know that last line is OK? Our thought process is something like this:
- We started with values in indices zero through three of
names. - sorting
namesrearranges elements without changing the set of valid indices. - Therefore, in the last line,
namesstill has an element at index3.
That’s just an informal proof, and we use that kind of reasoning every time we write a line of code. So, regular programming is on the same continuum as formally proving correctness. Let’s explore how injecting just a little more formalism into our process can enable Better Code.
Local Reasoning
The kind of everyday thinking demonstrated above is only practical if we can reason locally about code. The clearest definition of Local Reasoning we’ve found is this one from Nathan Gitter: “Local reasoning is the idea that the reader can make sense of the code directly in front of them, without going on a journey discovering how the code works.”
So in our example, what we know about sort allows us to reason about
its use without looking at its implementation. In fact, local
reasoning is so fundamental that most accepted best
practices exist just to support our ability to do it. It’s why we
use access control, why we break programs into components like
functions, types, and modules, and why we try to keep them small.
Components and Abstraction
Obviously, “knowing something about sort” is only possible and
relevant because sort is a separate component with its own name. If
we had inlined the sorting code directly into some other function,
we’d need to leave a comment for maintainers telling them
that the code was meant to sort. Even with the comment, to
fully understand the surrounding function, maintainers would
have to reason through the sorting algorithm’s details.
The comment itself is a code smell: every time we write a comment
that describes what the next piece of code is about to do, we should
ask whether that code can be its own function.
It’s a common fallacy that factoring out a component only makes sense if it’s used more than once. The real criterion is whether you can identify a unit of abstraction—the third ingredient of Better Code. If you can think of a simple and descriptive name for something, that’s usually a good indicator that there’s an abstraction waiting to be discovered.
In fact, creating a component creates an immediate opportunity for the code to be reused in tests. The more code you test separately, the better it is for correctness. The more you can use understandable abstractions, the simpler and clearer the use sites become, which is better for correctness. Abstraction and correctness support one another in a virtuous cycle. And abstraction is necessary for programming at scale: because we can’t keep the whole program in our heads, we do what humans always do when faced with complexity: we break complicated problems into parts that can be understood in isolation.
Hoare Logic
Programming by contract grew out of Hoare Logic, developed in 1969 by the British computer scientist and logician Tony Hoare. While we won’t be using Hoare Logic directly, we’ll discuss it briefly to provide historical context, and because it is the source of crucial terminology.
Preconditions and Postconditions
Hoare used this notation, called a “Hoare triple,”
{P}S{Q}
which is an assertion that if precondition P is met, operation S establishes postcondition Q.
For example:
-
if we start with
x == 2(precondition), afterx += 1,x == 3(postcondition):{x == 2}x+=1{x == 3}
-
if
xis less than the maximum integer (precondition), afterx += 1,xis greater than the minimum integer (postcondition):{x < Int.max}x+=1{x > Int.min}
What makes preconditions and postconditions useful for formal proofs is this sequencing rule:
{P}S{Q} ∧ {Q}T{R} ⇒ {P}S;T{R}
Given two valid Hoare triples, if the postconditions of the first are the preconditions of the second, we can form a new valid triple describing the effects of performing the operations in sequence. The notation may look exotic, but its meaning should be familiar: this rule simply captures the reasoning we use to think informally about statements when we write one statement after the next.
For example, take these two independent lines of code:
let m = (h - l) / 2
and
h = l + m
There are many valid Hoare triples for each of them. For instance,
{l+m=0}h = l + m{h≤0}. This one isn’t particularly
useful, but it is valid because if l + m == 0 is true before we
execute it, h <= 0 will be true afterwards.
The following—more useful—triples will help illustrate the sequencing rule:
-
{l≤h}
let m = (h - l )/2{m≥ 0}, i.e.// precondition: l <= h let m = (h - l) / 2 // postcondition: m >= 0 -
{m≥0}
h = l + m{l≤h}, i.e.// precondition: m >= 0 h = l + m // postcondition: l <= h
The fact that the first postcondition matches the second precondition means that the operations can be executed in sequence, with the sequence having the first precondition and the second postcondition. Thus there’s a new valid triple:
{l≤h}let m = (h -l )/2; h = l + m{l≤h}, i.e.
// precondition: l <= h
let m = (h - l) / 2
h = l + m
// postcondition: l <= h
which says that if l≤h is true on entry to the sequence, it is also true on exit.
Invariants
When a valid triple has identical precondition and postcondition, Hoare calls that condition an invariant of the operation, a condition that the operation preserves. The preceding example is distilled from the binary search algorithm, where l≤h is an invariant of the algorithm. Knowing that l≤h at each step is crucial to understanding the algorithm’s correctness.
The sequencing rule is good for code that runs in a straight line, but not all code is like that. Hoare also gave us a tool for reasoning about loops.
A loop invariant is a condition that holds just before a loop,
after each iteration, and just after the loop. In this linear search
there’s an invariant that no
element preceding the ith one is equal to x.
var i = 0
while (i != a.count && a[i] != x) {
i += 1
}
Knowing that’s always true when the loop exits allows us to conclude
that if i != a.count, the first occurrence of x in a is at
index i. Anything else would contradict the loop invariant.
Loops are difficult to reason about in general, which is why we suggest you avoid them (see the Algorithms chapter). But when you do have a loop, identifying its invariant is often the first step in understanding what it does.
Design By Contract
…a software system is viewed as a set of communicating components whose interaction is based on precisely defined specifications of the mutual obligations — contracts.
—Building bug-free O-O software: An Introduction to Design by Contract™ https://www.eiffel.com/values/design-by-contract/introduction/
In the mid 1980s, The French computer scientist Bertrand Meyer took Hoare Logic, and shaped it into a practical discipline for software engineering that he called “Design By Contract.” Meyer was focused on software components—types and methods. He realized that there’s special power in the Hoare triple that captures the intentions of a function’s author—the function’s contract:
- The precondition describes which calls to a function should be considered valid.
- The postcondition specifies only the function’s intended behaviors when correctly invoked and successful. Details such as the precise order of equivalent elements after an unstable sort, what happens when preconditions are violated, or which errors are reported (more on this in the next chapter) are omitted.
- It is general—describing the result for all inputs the author intends to support—so it can be applied in reasoning about any call to the function.
- An author can evolve the implementation so that valid uses by clients stay valid and retain their meaning.
Contracts describe the rules that govern how one piece of software talks to another. In other words, they’re relationships. Thinking in terms of relationships is one of the themes of Better Code, and you can expect us to point relationships out as they come up in our material.
Although a software component’s implementation must uphold its contract, it’s important to think of a contract as part of the component’s interface, since clients will depend on it.
Which code is to blame?
When something goes wrong in software, focusing on which person to blame is counterproductive, but deciding which code is to blame is the first step. Contracts tell us which code needs fixing:
-
If preconditions aren’t satisfied, that’s a bug in the caller. The function is not required to make any promises1 in that case.
-
If preconditions are satisfied but postconditions are not fulfilled, that’s a bug in the callee, or in something it calls.
And if you ever find yourself with something that’s clearly a bug but you can’t decide which code to blame, that’s a sign that a contract is missing or incomplete. A system that exposes these situations, where everybody is playing by the rules but things still go wrong, is known by the technical term “footgun.”
Type Invariants
The other contribution of Meyer’s Design by Contract was to apply the idea of “invariants” to types. A class invariant (or type invariant), is a condition that holds at a type’s public API boundary—whenever a client interacts with it. When we talk about an instance being “in a good state,” we mean that its type’s invariants are satisfied.
For example, this type’s public interface is like an array of pairs, but it stores elements of those pairs separate arrays.2
/// A series of `(X, Y)` pairs.
struct PairArray<X, Y> {
// Invariant: `xs.count == ys.count`
/// The first part of each element.
private var xs: [X] = []
/// The second part of each element.
private var ys: [Y] = []
/// An empty instance.
public init() {}
/// The `i`th element.
public subscript(i: Int) -> (X, Y) {
(xs[i], ys[i])
}
/// The length.
public var count: Int { ys.count }
/// Adds `e` to the end.
public mutating func append(_ e: (X, Y)) {
xs.append(e.0)
ys.append(e.1)
}
}
The invariant for this type is that the private arrays have the same
length. It’s important to remember that invariants only hold at a
type’s public interface boundary and are routinely violated,
temporarily, during a mutation. For example, in append, we have to
grow one of the arrays first, which breaks the invariant until we’ve
done the second append. That’s not a problem because the arrays are
private—that “bad” state is encapsulated by the type, and
invisible to clients. By the time we return from append, everything
is back in order.
Because we can control access to the visibility of “bad” states, there is a formula for upholding a type invariant. We
- make stored properties
privateorprivate(set) - establish the invariant in the type’s
initmethod(s), and - in
mutatingmethods and in the mutation clauses of subscripts and computed properties, ensure the invariant is intact:- before returning to the caller
- before passing
selfas a parameter - before calling a non-private method or subscript—or accessing a non-private computed property—of
self.
Non-mutating operations can’t disturb the invariant, and any mutating operation that only uses the type’s public API trivially upholds the invariant.
How To Choose a Type Invariant
Often, you have a choice about how strong to make your type’s
invariant. For example, because of the way PairArray was written,
it has an invariant that xs.count == ys.count, but if we add this
initializer, we’d have to weaken that invariant:
/// An instance with the value of `zip(xs, ys)`.
///
/// - Precondition: `xs.count <= ys.count`.
init(xs: [X], ys: [Y]) { (self.xs, self.ys) = (xs, ys) }
[Note: when sequences of unequal length are zipped, the result
has the length of the shorter sequence.]
Now the invariant becomes xs.count <= ys.count. We call that a
weaker invariant because it allows many more internal states. 3
Let’s see what effect the new invariant would have on the rest of the
implementation. Most of it is unchanged, but to maintain the same
meaning, the append method must account for these new internal
states. Here’s one way we could do it:
/// Adds `e` to the end.
public mutating func append(_ e: (X, Y)) {
ys.removeRange(xs.count...) // <=====
xs.append(e.0)
ys.append(e.1)
}
Also, the count property needs to be changed to return xs.count
instead of ys.count.
As is often the case, a weaker invariant complicated the implementation and made it more fragile. Here, only two adjustments were needed, but in principle there could be many more.
Instead of allowing the invariant to be weakened, we can adjust the new initializer to maintain the original invariant, thus avoiding downstream consequences for the rest of the implementations. There are two simple approaches:
-
We can strengthen the precondition of the new
initmethod, requiring thexsandysarguments to have the same length:/// An instance with the value of `zip(xs, ys)`. /// /// - Precondition: `xs.count == ys.count`. init(xs: [X], ys: [Y]) { (self.xs, self.ys) = (xs, ys) } -
Or, we can remove the precondition entirely and normalize the internal representation upon construction:
/// An instance with the value of `zip(xs, ys)`. init(xs: [X], ys: [Y]) { (self.xs, self.ys) = (xs, ys) let l = Swift.min(xs.count, ys.count) self.xs.removeRange(l...) self.ys.removeRange(l...) }
Either approach is acceptable, and ordinarily one should favor the
stronger precondition, which restricts the valid inputs to ones that
are obviously meaningful, and which doesn’t add potentially-costly
normalization steps. In this case, however, there is an existing
operation, zip—which does not impose a precondition—whose semantics
we can match and use to describe the results of our new API. The
normalization approach yields a more coherent system with a simpler
contract for the new API.
It’s Documentation
Looking back at Meyers’ definition, you might notice he says contracts are “precisely defined specifications,” which is just a fancy word for documentation. Undocumented software is neither correct nor incorrect; it does something, but does it do the right thing? There’s no way to know.
All undocumented software is waste. It’s a liability for a company.
—Alexander Stepanov (https://youtu.be/COuHLky7E2Q?t=1773)
Documentation is also essential for local reasoning. We build atop libraries and a programming language, which run on an operating system, which runs on computer hardware, which ultimately depends on the laws of physics. So what keeps us from recursing, non-locally, down to the limits of known physics when we think about how a program works?
Building a Tower of Abstraction
The answer is documentation. We can use libraries and our programming language without digging into their implementations because they are documented. Compiler engineers can do their jobs because the hardware manufacturers document the architecture and instruction sets. Hardware designers succeed because physicists document the laws of physics. That’s the tower we’re building. Each layer of documentation defines an abstraction boundary that eliminates the need to research an implementation when reasoning about code. Without it, quality software development at scale becomes infeasible.
The catch is that the code you are writing is almost never at the top of the tower. Someone else, or future-you, is going to have to build on the code you’re writing. Thus, there’s no difference between public and private interfaces where documentation is concerned. Every component is API to somebody, so when we say “API” we mean any interface boundary in the codebase, however internal.
Contract Checking Features
You may have heard that some languages have features to support “Design by Contract.” In general, that means you can write parts of your contracts as code, and get some checking at runtime to make sure these contracts are upheld. 4
struct MyArray<T> {
...
// Returns the `i`th element.
@requires(i >= 0 && i < self.count)
fun getNth(i: Integer): T
...
}
The idea started with Bertrand Meyer’s own Eiffel language, and was picked up by many others, including D, Scala, Kotlin, and Clojure. Other languages, like Rust and Python, have contract libraries that provide a near-native contract programming experience.
If you use one of these languages, fantastic; absolutely do leverage those features and libraries. That can reduce the amount of pure documentation you need to write and add automated contract checking at runtime. But there are caveats:
-
Contracts are fundamentally documentation, even if they’re expressed in code, so they must appear in the API descriptions consumed by client programmers. If you’re using contract checking with automated documentation extraction tools make sure they expose the contract code along with the API. If not, you need to repeat the complete contract in documentation.
-
Some contracts are better expressed in human language than as code,
-
Some contracts are impossible to express as code, or impossible to express efficiently. We’ll see an example in a moment.
We’ll discuss contract checking as a separate topic in the next chapter.
Document Everything
So every declaration needs to be documented. We realize that’s not
standard industry practice, and to many people it sounds like a huge
burden! Read on, though, and we’ll show you how it can be done both
thoroughly and economically. To see that it’s possible, look again
at the documentation shown for PairArray. It tells you everything
you need to know to use or test each declared element, but is not
burdensome to write.
Comprehensively documenting your declarations is not only practical and necessary, but it has benefits beyond correctness and local reasoning: it helps you improve your APIs and can even increase overall development speed. But you’ll only see those benefits if you document as you code. If documentation is an afterthought, it can only ever be experienced as overhead.
If you take this discipline seriously, you’ll begin to notice that the poorly designed or named components haven’t been documented clearly and concisely. Sometimes that documentation is obviously unachievable because the design simply has too many wrinkles. In other cases you’ll find the design can be salvaged by driving it towards a simpler contract. Improvements in development speed come from arriving at the right design earlier, since a poor experience for readers of your documentation becomes a “code smell.”
Use Documentation Comments
Contract documentation should always go in your code as comments, because:
-
That puts the two things that should correspond—documentation and implementation—next to one another, so you can see when they don’t match up. People sometimes complain that docs go out of date, but that’s exactly the point: without the ability to see that inconsistency, there’s no way to know that there’s a bug.
-
Using comments makes it reasonable to combine the activities of coding and documentation, which are mutually supportive.
-
IDEs and documentation extractors recognize special
///comment syntax to provide an improved developer experience. Using the recognized syntax opens the door to other forms of processing in the future.
On Code Review
Contracts also deserve early attention in code reviews. The first step should be to review the APIs: all declarations (e.g. function signatures) and their associated contracts. A change that omits documentation for any declaration should be sent back for revision, because there’s no way to make a judgement about that component’s correctness.
Then, if the APIs aren’t well-documented (and designed), there’s
little point in looking at their implementations. Remember, APIs are
the connective tissue; they’re what every client of a component
interacts with, and will have effects throughout the codebase.
A deficient implementation can only do local damage, but a deficient
contract or design can cause unbounded technical debt.
A Tower of Invariants
The tower of abstraction mentioned earlier comes with a tower of
invariants. The invariants of PairArray are built on—and depend
on—the invariants of the individual arrays. We can embed PairArray
into some larger data structure with its own invariant, which will
depend on those of PairArray.
In fact, you can think of the entire state of your program as one big data structure with its own invariants, and formalize what it means for the program to “be in a good state” that way. For example, you might have a database of employees, each of which has its own ID, and which also stores the ID of the employee’s manager (the CEO gets to be their own manager).
It’s an invariant of your program that a manager ID can’t just be random; it has to identify an employee that’s in the database—that’s part of what it means for the program to be in a good state, and all through the program you have code to ensure that invariant is upheld.
Encapsulating invariants
It would be a good idea to identify and document that whole-program
invariant in some central location, so maintainers will know to uphold it, and that they
can rely on it. An even better idea, though, is to encapsulate the invariant in a type,
and document that. So instead of using an SQLDatabase type
directly, you could create an EmployeeDatabase type with a private
SQLDatabase, whose public API always upholds that invariant. Now
you can remove the invariant-upholding logic from the rest of your
code. This is one of the most powerful code transformations you can
make.
/// The employees of a company.
///
/// Invariant: every employee has a manager in the database.
struct EmployeeDatabase {
/// The raw storage.
private var storage: SQLDatabase;
/// Adds a new employee named `name` with manager `m`, returning the
/// new employee's ID.
///
/// - Precondition: `m` identifies an employee.
public addEmployee(_ name: String, managedBy m: EmployeeID) -> EmployeeID
/// Removes the employee identified by `e`.
///
/// - Precondition: `e` identifies an employee who is not the
/// manager of any other employee.
public remove(_ e: EmployeeID)
...
}
Upholding invariants is the entire purpose of access control, so use
private whenever you can!
The Power of Type Information
Everything you see in a function signature is implicitly part of the
function’s contract. A function with a parameter of type
EmployeeDatabase has a precondition that the database upholds the
manager invariant, but it doesn’t need to be stated explicitly; it’s
enforced automatically by the compiler and the implementer of
EmployeeDatabase. So static typing gives you a leg up on the
“document everything” project. If you were programming in a totally
dynamic language, like Javascript, or Python without type hints, you
have to put a lot more of that sort of information into the written
documentation.
Public And Private Invariants
Some invariants, such as the one documented for EmployeeDatabase
above, are exposed to users of the type, and thus should be in public
doc comments. Others, like the invariant that the members of
PairArray have the same length, are purely part of its
implementation, and should be encoded in ordinary comments addressed
privately to the maintainer of the code. Note that you
can have both: PairArray also has a public invariant that its
count is non-negative. We’ll get to why this particular invariant
is not explicitly documented in a moment…
Making It Tractable
If we’re going to integrate documentation into our programming so tightly, we need to make sure it’s neither intrusive for the maintainer to read nor burdensome for the author to write. Ideally, it should help both of them.
Now, not every contract is as simple as the ones we’ve shown so far,
but simplicity is a goal. In fact, if you can’t write a terse,
simple, but complete contract for a component, there’s a good chance
it’s badly designed. A classic example is the C library realloc
function, which does at least three different things—allocate, deallocate, and resize
dynamic memory—all of which
need to be described. A better design would have separated these
functions. So simple contracts are both easy to digest and easy to
write.
Let’s look at another example. The methodology we’ll be following here is based on the Swift API Design Guidelines, only slightly modified.
/// A resizable random-access `Collection` of `T`s.
struct MyArray<T> {
/// Removes and returns the last element.
public mutating func popLast() -> T { ... }
/// The `i`th element.
public subscript(i: Int) -> T { ... }
/// The length.
public var length: Int { ... }
.
.
.
/// The number of elements that can be stored before storage is
/// reallocated.
private var capacity: Int { ... }
}
Basics
We’ve started by filling in the most basic part of every documentation comment: a summary sentence fragment that minimally describes what the thing is or does.
The first one
/// A resizable random-access `Collection` of `T`s.
struct MyArray<T>
gives us the context we need to understand the methods: we’re looking at the declaration of dynamic array type that holds any number of Ts.
Now let’s look at the documentation for the first method, called “popLast.”
/// Removes and returns the last element.
public mutating func popLast() -> T { ... }
As you can see from the summary, it removes and returns the last
element. Notice that the phrase “last element” is meaningful only
because we documented that DynamicArray is a Collection, i.e.
a Sequence of elements that supports multiple iteration passes.
The summary for a method should tell us what the method returns, and what its other effects are, if any. This method is a little unusual in that it both mutates the array and returns a result, which means you need to decide what to emphasize in the summary: the removal or the returned value. Here we’ve emphasized the mutation, which is normally what you want. That emphasis is reflected by the name, which describes the mutation rather than the return value.
You’d generally only emphasize the return value if the mutation is something incidental that doesn’t affect the program’s meaning, like updating a cache.
Notice also that we’ve omitted needless words: we didn’t write, “the
last element of the DynamicArray,” because the type sets the context
for its methods. Remember, every word the reader has to process takes
mental energy and contributes to perceived complexity. We use all the
necessary words, but no more than necessary.
Details
Let’s continue by spelling out the preconditions, postconditions,
and invariants of popLast().
What are the preconditions for removing an element? Obviously, there needs to be an element to remove.
/// Removes and returns the last element.
///
/// - Precondition: `self` is non-empty.
public mutating func popLast() -> T { ... }
A client of this method is considered to have a bug unless the array has an element. OK, so what about postconditions?
The postconditions are the effects of the method plus any returned result. If the preconditions are met, but the postconditions are not, and the function does not report a runtime error, we’d say the method has a bug. The bug could be in the documentation of course, which is a part of the method.
/// Removes and returns the last element.
///
/// - Precondition: `self` is non-empty.
/// - Postcondition: The length is one less than before
/// the call. Returns the original last element.
public mutating func popLast() -> T { ... }
The invariant of this function is the rest of the elements, which are unchanged:
/// Removes and returns the last element.
///
/// - Precondition: `self` is non-empty.
/// - Postcondition: The length is one less than before
/// the call. Returns the original last element.
/// - Invariant: the values of the remaining elements.
public mutating func popLast() -> T { ... }
Now, if the postcondition seems a bit glaringly redundant with the summary, that should be no surprise. The summary of a method should describe what the method does, and what it should return. That’s the postcondition.
In fact, it’s very rare that a postcondition will need to be separately documented. The only reason you might write it out is if there’s some aspect of the postcondition you can’t easily capture in the summary (and remember, the postcondition that can’t be fully described in the summary is a “code smell”, thus these cases are rare).
Therefore we can erase the separate statement of postconditions. That said, you should always ask yourself how you’d state the postconditions and making sure they’re completely captured by the summary before you omit them. Considering the postcondition is part of the process that makes the summary complete.
And since we know everything the method does is captured in the summary, we can assume everything else in the program is unchanged, so the invariant is also trivially implied. And that is also very commonly omitted.
/// Removes and returns the last element.
///
/// - Precondition: `self` is non-empty.
public mutating func popLast() -> T { ... }
In fact, the precondition is implied by the summary too. You can’t remove and return the last element if there’s no last element, right?
Whether or not to omit an implied precondition may be a slightly different judgement from the others, because it’s information every client needs in order to use the function correctly. We recommend omitting explicit documentation of any implied precondition unless it’s quite subtle. With that policy, a client must assume that anything required for the summary to make sense is a precondition. In this case it’s not subtle that having a last element is required if you are going to remove the last element, so the original declaration should be sufficient:
/// Removes and returns the last element.
public mutating func popLast() -> T { ... }
In practice, once you are comfortable with this discipline, the thought process behind writing this documentation is straightforward; it need not take more than a few seconds:
- Write a summary.
- If it doesn’t completely describe the postconditions
- Try to make it do so, or if that is just too awkward,
- add a postcondition clause.
- If there are any preconditions not straightforwardly implied by the summary, add a precondition clause.
A More Complicated Example
Let’s take a look at a traditional sorting algorithm on a fictitous collection type:
extension DynamicArray {
/// Sorts the elements so that `areInIncreasingOrder(self[i+1],
/// self[i])` is false for each `i` in `0 ..< length - 2`.
///
/// - Precondition: `areInIncreasingOrder` is a strict weak ordering
/// over the elements of `self`.
/// - Complexity: at most N log N calls to `areInIncreasingOrder`, where N is
/// the number of elements.
mutating func sort(areInIncreasingOrder: (Element, Element)->Bool) { ... }
}
var a = [7, 9, 2, 7]
a.sort(areInIncreasingOrder: <)
print(a) // prints [2, 7, 7, 9]
This method sorts the elements according to some comparison predicate
areInIncreasingOrder. So if we pass it the less-than operator,
which is true when the first argument is less than the second, we get
the elements arranged from least to greatest. The contract gives an
explicit precondition that isn’t implied by the summary: it requires
that the predicate be a strict weak ordering.
Some precondition on the predicate is needed just to make the result
a meaningful sort with respect to the predicate. For example, a
totally unconstrained predicate could return random boolean values,
and there’s no reasonable sense in which the function could be said to
leave the elements sorted with respect to that. Therefore the
predicate at least has to be stable. To leave elements meaningfully
sorted, the predicate has to be transitive: if it is true for
elements (i, j), it must also be true for elements (i, j+1).
A strict weak ordering has both of these properties, among others.
Note that the performance of this method is documented. Time and space complexity have to be part of the contract if you want your clients to be able to reason locally about the performance of their own code. In fact, we can view the performance contract of the function as part of its postconditions, which brings all the function’s guarantees under one name: its postconditions.
The strict weak ordering requirement is a great example of a precondition that can’t be efficiently checked. To do so would require at least N² comparisons, where N is the number of elements, which would violate the complexity bound of the algorithm.
The summary gives the postcondition that no two adjacent elements are out-of-order according to the predicate. The contract is perfectly correct, but it’s awkward and complex. To deal with the case where elements are equal, the postcondition has to compare adjacent elements in reverse order and negate the predicate. If we had just written:
/// Sorts the elements so that `areInIncreasingOrder(self[i],
/// self[i+1])` is true for each `i` in `0 ..< length - 2`.
Our example result [2, 7, 7, 9] would fail to satisfy the
postcondition because of the adjacent 7s.
The term “strict weak ordering,” which is not very well-known or understood, is another source of complexity. In fact we should probably put a link in the documentation to a definition.
/// - Precondition: `areInIncreasingOrder` is [a strict weak
/// ordering](https://simple.wikipedia.org/wiki/Strict_weak_ordering)
/// over the elements of `self`.
We don’t normally add examples to our documentation—it makes documenting more laborious and can actually distract from the essential information—but because the statement of effects is tricky, this is a case where an example might really help.
/// Sorts the elements so that `areInIncreasingOrder(self[i+1],
/// self[i])` is false for each `i` in `0 ..< length - 2`.
///
/// var a = [7, 9, 2, 7]
/// a.sort(areInIncreasingOrder: <)
/// print(a) // prints [2, 7, 7, 9]
///
/// - Precondition: `areInIncreasingOrder` is [a strict weak
/// ordering](https://simple.wikipedia.org/wiki/Strict_weak_ordering)
/// over the elements of `self`.
/// - Complexity: at most N log N comparisons, where N is the number
/// of elements.
mutating func sort(areInIncreasingOrder: (Element, Element)->Bool) { ... }
Letting Simplicity Drive Design
Let’s take the complexity of the documentation as a hint that the API
could be improved and see where that leads. If sort had been
written to work with <= as an argument, producing the same result,
the summary could have avoided swapping elements in the comparison and
negating the result:
/// Sorts the elements so that `areInOrder(self[i],
/// self[i+1])` is true for each `i` in `0 ..< length - 2`.
If we view a strict weak ordering as a generalization of what < does, the
corresponding generalization of <= is called a total preorder. 5 In fact,
we can convert any total preorder to a corresponding strict weak order
(and vice-versa), with this function.
/// Returns the converse of `f`'s complement, `g(x, y) := !f(y, x)`.
func converseOfComplement<T>(_ f: @escaping (T, T)->Bool)
-> (T, T)->Bool
{
{ x, y in !f(y, x) }
}
Therefore, if we have a sorting implementation that works with any
strict weak order, we can easily convert it to work with any total
preorder by passing the predicate through converseOfComplement.
Note that the name of the predicate became simpler: it no longer tests that its arguments represent an increase. Instead, it tells us whether the order is correct. Because the summary is no longer tricky, we can drop the example, and we’re left with this:
/// Sorts the elements so that `areInOrder(self[i],
/// self[i+1])` is true for each `i` in `0 ..< length - 2`.
///
/// - Precondition: `areInOrder` is a [total
/// preorder](https://en.wikipedia.org/wiki/Weak_ordering#Total_preorders)
/// over the elements of `self`.
/// - Complexity: at most N log N comparisons, where N is the number
/// of elements.
mutating func sort(areInOrder: (Element, Element)->Bool) { ... }
But we can go further and use a much simpler and more natural summary:
/// Sorts the elements so that all adjacent pairs satisfy
/// `areInOrder`.
Usually, the less our documentation looks like code (without sacrificing precision), the greater its power as a tool for understanding programs, writing tests, and verifying correctness. 6.
There are two further simplifications we can make to the
precondition. First, everything in documentation of a method happens
in the context of self, so we can drop “of self.” This
improvement was available to us all along, and may seem trivial, but
the value of omitting needless
words
adds up over lots of function definitions. It lowers the burden of
writing documentation, and makes it much easier to read. Remember,
every word a reader has to process has a cognitive cost in addition to
whatever benefits it may provide, so each one should more than pay for
itself.
Secondly, the summary has become so simple that we can embed the precondition there without overly complicating it, making the final declaration:
/// Sorts the elements so that all adjacent pairs satisfy the [total
/// preorder](https://en.wikipedia.org/wiki/Weak_ordering#Total_preorders)
/// `areInOrder`.
///
/// - Complexity: at most N log N comparisons, where N is the number
/// of elements.
mutating func sort(areInOrder: (Element, Element)->Bool) { ... }
There is one factor we haven’t considered in making these changes:
sorting functions taking strict weak orderings are traditional. One
has to weigh the risk that a user reflexively writes sort(areInOrder: <) against the value of these simplifications. The argument label
helps a bit, because if you happen to consider the equal elements
case, you will see they clearly “areInOrder” and will realize <
returns false for equal elements. But if you don’t happen to consider
that case, you won’t get the sort you expected… sometimes. Whether to
break with precedent in order to get a simpler API with a clearer
contract is an engineering decision you will have to make. To reduce
the risk you could add this assertion7, which will stop the program if
the ordering is strict-weak:
precondition(
self.isEmpty || areInOrder(first!, first!),
"Total preorder required; did you pass a strict-weak ordering?")
The Moral of the Story
The dramatic cascade of API improvements we saw with the sort
example was a direct consequence of writing and evaluating the
contract. It’s easy—and quite common—to put off writing documentation,
but the transformations we saw here are only possible if documenting
contracts is treated not as an afterthought, but as a fundamental part
of the API design process.
Project-Wide Documentation Policies
The reason you haven’t seen complexity documented in examples before
sort is that we have a policy that operations have constant
complexity unless specifically documented otherwise. Making “document
everything” practical depends on having some well-chosen project-wide
policies that save you from repeating common patterns.
Your project’s choice of policy can make the difference between documentation being useful and being burdensome or inconsistent (at which point people will just stop reading and writing it). Even more than your choice of policies, it’s important above all that you have policies written down where maintainers can refer to them.
For example,
Every declaration outside a function body must have a documentation comment that describes its contract.
Start with a summary sentence fragment.
- Describe what a function or method does and what it returns.
- Describe what a property or type is.
- Separate the fragment from any additional documentation with a blank line and end it with a period.
Preconditions, postconditions and invariants obviously implied by the summary need not be explicitly documented.
Declarations that fulfill protocol requirements are exempted when nothing useful can be added to the documentation of the protocol requirement itself.
Document the performance of every operation that doesn’t execute in constant time and space.
It is reasonable to put information in the policies without which the project’s other documentation would be incomplete or confusing, but you should be aware that it implies policies must be read. We recommend embedding these policies in your project’s coding standard document, near the top where it is less likely to be treated as an afterthought.
Code and Contract Evolution
One of the most powerful features of contracts is that they allow us to replace the implementation of a function without breaking its correct clients. As long as the new implementation fulfills the postconditions, and relies on nothing more than the preconditions, those clients will continue to function as before. Again, this is local reasoning in action: we can reason about changes to a given function without going on a journey to inspect all of its callers.
But suppose you want to change a function’s contract? The correctness-preserving changes are those that weaken the function’s preconditions and/or strengthen its postconditions. For example, this method returns the number of steps from the start of a collection to an occurrence of some value.
extension Collection where Element: Equatable {
/// Returns the number elements between the start and an occurrence
/// of the value `x`.
///
/// - Precondition: `self` contains `x`.
/// - Complexity: at most N comparisons, where N is the number
/// of elements.
func offsetFromStart(_ x: Element) -> Int {...}
}
Suppose we weaken the postcondition?
extension Collection where Element: Equatable {
/// Returns a value less than or equal to the number elements
/// between the start and an occurrence of the value `x`.
///
/// - Precondition: `self` contains `x`.
/// - Complexity: at most N comparisons, where N is the number
/// of elements.
func offsetFromStart(_ x: Element) -> Int {...}
}
Before the change, clients had a right to expect this assertion to pass:
assert(
c[c.index(c.startIndex, offsetBy: c.offsetFromStart(x))] == 'x`)
but with the new contract a correct implementation of the method could
be { return 0 }, and that assertion would surely fail for many valid
inputs to the function, e.g. [1, 2, 3].offsetFromStart(3)
Strengthening the precondition has a similar effect:
extension Collection where Element: Equatable {
/// Returns the number elements between the start and an occurrence
/// of the value `x`.
///
/// - Precondition: `self` contains `x` and `x` != `first`.
/// - Complexity: at most N comparisons, where N is the number
/// of elements.
func offsetFromStart(_ x: Element) -> Int {...}
}
After that change, the following line of code has developed a new bug!
let i = [1, 2, 3].offsetFromStart(1) // 1 == [1, 2, 3].first
The ability to (non-locally!) break all the clients of a function means we must be extraordinarily careful to follow the rules when evolving a contract. Now let’s see how that plays out.
Strengthening the postcondition might look like this:
extension Collection where Element: Equatable {
/// Returns the number elements between the start and the first
/// occurrence of the value `x`.
///
/// - Precondition: `self` contains `x`.
/// - Complexity: at most N comparisons, where N is the number
/// of elements.
func offsetFromStart(_ x: Element) -> Int {...}
}
We now guarantee we’ll find the first occurrence of x, rather than
just any occurrence. Since clients had no right to expect anything
other than the first occurrence, no clients can be broken by this
change.
From there, let’s weaken the precondition by simply removing it:
extension Collection where Element: Equatable {
/// Returns the number elements between the start and the first
/// occurrence of the value `x`, or `count` if `x` is not found.
func offsetFromStart(_ x: Element) -> Int {...}
}
If you read carefully, this might appear to also be a weakening of the
postcondition. After all, before the change, clients had a right to
expect never to get count as a result. However, that new
possibility only occurs in cases that were disallowed before the
change. Clients that were correct before the change had to ensure
that x was contained in the collection, and those clients cannot
observe a result of count unless they adjust their code to take
advantage of the weakened precondition.
Remember that performance guarantees are part of a function’s postconditions: without breaking client code, they can be strengthened to promise more efficiency, but never weakened.
Polymorphism and Higher-Order Functions
Similar rules apply to the contracts for protocol conformances: a method satisfying a protocol requirement can have weaker preconditions and/or stronger postconditions than required by the protocol:
Here, the conformance strengthens the performance guarantee given by the protocol requirement:
protocol Searchable: Collection {
/// Returns the first position where `x` occurs.
///
/// - Complexity: at most `count` comparisons and index adjustment
/// calls.
func index(of x: Element) -> Index
}
extension SortedArray: Searchable {
/// Returns the first position where `x` occurs.
///
/// - Complexity: at most log2(`count`) comparisons and index
/// adjustment calls.
func index(of x: Element) -> Index
}
Function parameters of function type act similarly: a function or closure passed as an argument can have weaker preconditions and/or stronger guarantees than those specified by the callee. Take this function for instance:
extension Collection {
/// Returns a mapping from `bucket(x)` for all elements `x`, to the
/// set of elements `y` such that `bucket(y) == bucket(x)`.
///
/// - Precondition: `bucket(x)` is non-negative for all elements `x`.
func bucketed(per bucket: (Element)->Int) -> [Int: Set<Element>]
{ ... }
}
There’s a requirement8 on the postcondition of bucket: it
must return only numbers greater than -1. This call passes a function
that returns only 0 or 1; a strictly stronger postcondition:
(0..<10).bucketed { $0 % 2 }
That call is clearly fine. This one, however, passes a function whose
postcondition is neither identical nor strictly stronger. That
violates the requirements of bucketed:
(0..<10).bucketed { $0 - 2 }
Conclusion
Programming by contract establishes a framework for understanding correctness, for knowing what to test, for reasoning locally about program semantics, for distributing work across programmers, for evolving existing code, and even for evaluating API quality. It is a practical discipline built on the kind of informal reasoning every programmer applies daily.
By treating contract documentation as essential infrastructure—the connective tissue of our software—and making it part of the design process, we can write Better Code the first time around, and build programs efficiently, at scale.
As you apply these ideas in your own work, remember that like any powerful tool, contracts require practice to master. Start small: document all new declarations, think explicitly about preconditions and postconditions, and design your types to maintain clear invariants. Existing components with insufficient contract documentation create a cycle of repeated reverse-engineering. When you discover how these components work, instead of throwing that work away, capture it in new documentation. As these practices become natural, you’ll find yourself writing not just better code, but thinking more clearly about the problems you’re solving.
-
In fact, a function shouldn’t make any promises in case of precondition violation, because then the caller has a right to call the function and expect the promised result: it’s no longer a precondition violation. There is one exception, especially in a safe-by-default language like Swift: all functions not explicitly labeled unsafe implicitly promise not to have arbitrary “undefined behavior” when their preconditions are violated. But that’s a blanket guarantee that you don’t spell out in contracts. ↩
-
You might want to use a type like this one to store
(Bool, Int64)pairs without wasting storage for padding, or to leverage SIMD operations. ↩ -
If we removed the precondition on this initializer, without changing its implementation, the invariant would become empty, or
true, which is the weakest possible invariant: it allows the lengths ofxsandysto be completely unrelated. ↩ -
Languages exist that check contracts at compile time, such as Dafny and Lean 4, but we do not think they are practical tools for programming at scale. ↩
-
The generalization allows unequal elements to be given the same ”rank“ in the ordering. For example, if you sort integers by their absolute values, 2 and -2 will be ranked the same. ↩
-
One could argue that to not lose precision, the documentation needs to say in which order the adjacent elements are passed to
areInOrder. Judgement calls are part of engineering, and in our judgement the order of arguments is implied. ↩ -
See the next chapter for more on checking contracts at runtime. ↩
-
The requirement is, as usual for function requirements, stated as a precondition. We avoid using the word “precondition” here to make it clearer that we’re discussing the contract required of the function’s parameter,
bucket. ↩
Errors
In the Contracts chapter you may have noticed we made this reference to the concept of errors:
If the preconditions are met, but the postconditions are not, and the function does not report a runtime error, we’d say the method has a bug.
In the interest of progressive disclosure, we didn’t look closely at the idea, because behind that simple word lies a chapter’s worth of discussion. Welcome to the Errors chapter!
Definitions
To understand any topic, it’s important to have crisp definitions of the terms you’re using, and ideally, to take those definitions from the most common existing practice. Unfortunately “error” and associated words have been used rather loosely, and previous attempts to define these words have relied on other words, like “expected,” which themselves lack clear definitions, at least when it comes to programming.
Unless we want to invent new terms, we will have to impose a little of our own structure on the usual terminology. We hope these definitions are at least consistent with your understanding:
Error: anything that prevents a function from fulfilling its postcondition.
When we write the word “error” in normal type, we mean the idea above,
distinct from the related Swift Error protocol, which we’ll always
spell in code font.
Errors come in two flavors:1
Programming error, or bug: code contains a mistake. For example, an
ifstatement tests the logical inverse of the correct condition.Runtime error: a function could not fulfill its postconditions even though its preconditions were satisfied. For example, writing a file might fail because the filesystem is full.
Error Recovery
Let’s begin by talking about what it means to “recover from an error.”
An early use
of the term “error
recovery” was in the domain of compilers, where the challenge, after
detecting a flaw in the input, is to continue to process the rest of
the input meaningfully. Consider a simple syntax error: the simplest
possibilities are that the next or previous symbol is extra, missing,
or misspelled. Guessing correctly affects not only the quality of the
error message, but also whether further diagnostics will be
useful. For example, in this code, the while keyword is misspelled:
func f(x: inout Int) {
whilee x < 10 {
x += 1
}
}
As of this writing, the Swift compiler treats whilee as an
identifier rather than a misspelled keyword, and issues five unhelpful
errors, four of which point to the remaining otherwise-valid code.
That’s not an indictment of Swift; doing this job correctly is
nontrivial.
More generally, it has been said that recovering from an error allows a program to “to sally forth, entirely unscathed, as though ‘such an inconvenient event’ never had occurred in the first place.”
Being “unscathed” means two things: first, that the program state is intact—its invariants are upheld so code is not relying on any newly-incorrect assumptions. Second, it means that the state makes sense given the correct inputs received so far. “Making sense” is a subjective judgement. For example:
-
The initial state of a compiler, before it has seen any input, meets the compiler’s invariants. But when a syntax error is encountered, resuming from its initial state would discard the context seen so far. Unless the input following the error would have been legal at the beginning a source file, the compiler will issue many unhelpful diagnostics for that following input. Recovery means accounting somehow for the non-erroneous input seen so far and re-synchronizing the compiler with what follows.
-
In a desktop graphics application, it’s not enough that upon error (say, file creation fails), the user has a well-formed document; an empty document is not an acceptable result. Leaving them with a well-formed document that is subtly changed from its state before the error would be especially bad. “Recovery” in this case means preserving the effects of actions issued before the last one, so the document appears unchanged.
What About Recovery From Bugs?
We’ve just seen examples of recovery from two kinds of runtime error. What would it mean to recover from a bug? It’s not entirely clear.
First, the bug needs to be detected, and that is not assured. As we saw in the previous chapter, not all precondition violations are detectable. Also, it’s important to admit that when a precondition check fails, we’re not detecting the bug per-se: since bugs are flaws in code, truly detecting bugs involves program analysis. Instead, a runtime check detects a downstream effect that the bug has had on data. When we observe that a precondition has been violated, we know there is invalid code, but we don’t know exactly where it is, nor can we be sure of the full extent of damaged data.
So can we “sally forth unscathed?” The problem is that we can’t know. Since we don’t know where the bug is, the downstream effects of the problem could have affected many things we didn’t test for. Because of the bug, your program state could be very, very scathed indeed, violating assumptions made when coding and potentially compromising security. If user data is quietly corrupted and subsequently saved, the damage becomes permanent.
Unless the program has no mutable state and no external effects, the only principled response to bug detection is process termination. 2
As terrible as sudden termination may be, it’s better than the alternative. Attempting to recover means adding code, and recovery code is almost never exercised or tested and thus is likely wrong, and the consequences of a botched recovery attempt can be worse than termination. To no advantage, recovery code tends to obscure the surrounding logic and adds needless tests, which hurts performance. Continuing to run after a bug is detected also hurts our ability to fix the bug. When a bug is detected, before any further state changes, we want to immediately capture as much information as possible that could assist in diagnosis. In development that typically means dropping into a debugger, and in deployed code that might mean producing a crash log or core dump. If deployed code continues to run, the bug is obscured and—even if automatically reported—will likely be de-prioritized until it is less fresh and thus harder to address. Worse, it can result in multiple symptoms that will be reported as separate higher-priority bugs whose root cause could have been addressed once.
How to Handle Bugs
When a bug is detected, the best strategy is to stop the program before more damage is done to data and generate a crash report or debuggable image that captures as much information as is available about the state of the program so there’s a chance of fixing it.
Many people have a hard time accepting the idea of voluntarily terminating, but let’s face it: bug detection isn’t the only reason the program might suddenly stop. The program can crash from an undetected bug in unsafe code… or a person can trip over the power cord, or the operating system itself could detect an internal bug, causing a “kernel panic” that restarts the hardware. Software should be designed so that sudden termination is not catastrophic for its users.
In fact, it’s often possible to make restarting the app a completely seamless experience. On an iPhone or iPad, for example, to save battery and keep foreground apps responsive, the operating system may kill your process any time it’s in the background, but the user can still “switch back” to the app. At that point, every app is supposed to complete the illusion by coming back up in the same state in which it was killed. So non-catastrophic early termination is something you can and should design into your system. 3 When you accept that sudden termination is part of every program’s reality, it is easier to accept it as a response to bug detection, and to mitigate the effects.
Checking For Bugs
While, as we’ve seen, not all bugs are detectable, detecting as many as possible at runtime is still a powerful way to improve code, by detecting the presence of coding errors close to their source and incentivizing fixes.
Precondition Checks
Swift supplies a function for checking that a precondition is upheld, which can be used as follows:
precondition(n >= 0)
or
precondition(n >= 0, "n == \(n); it must be non-negative.")
In either case, if the condition is false, the program will be terminated (or stop if run in a debugger). 4 In debug builds, the file and line of the call will be written to the standard error stream, along with any message supplied. In release builds, to save on program size, nothing is printed and any expression passed as a second argument is never evaluated.
Assertions
Swift supplies a similar function called assert, modeled on the one
from the C programming language. Its intended use is as a “soundness
check,” to validate your own assumptions rather than to make contract
checks at function boundaries. For example, in the binary search
algorithm mentioned in the previous chapter,
// precondition: l <= h
let m = (h - l) / 2
h = l + m
// postcondition: l <= h
There is no contract supplying the Hoare-style precondition and postcondition you see there; they are internal to a single function. If violated, they indicate we’ve failed to understand the code we’ve written: the informal proof we used to evaluate the function’s correctness was flawed. Replacing those comments with assertions can help us uncover those flaws during testing of debug builds without impacting performance of release builds:
assert(l <= h)
let m = (h - l) / 2
h = l + m
assert(l <= h, "unexpected h value \(h)")
Similarly, assert can be useful for ensuring loop invariants are
correct (see the algorithms chapter). When trying to track down a
mysterious bug, adding as many assertions as possible in the problem
area can be a useful technique for narrowing the scope of code you
have to review.
Assertions are checked only in debug builds, compiling to nothing in
release builds, thereby encouraging liberal use of assert without
concern for slowing down release builds.
Postcondition and Expensive Precondition Checks
Checking postconditions is the role of unit tests and can be
compute-intensive, so in most cases we recommend leaving postcondition
checks out of function bodies. However, if you can’t be confident
that unit tests cover enough cases, using assert for some
postcondition checks in function bodies ensures there is no cost in
release builds.
Similarly, a precondition that can only be checked with a significant
cost to performance could be checked with assert. Because—unlike
most uses of assert—a precondition failure indicates a bug in the
caller, it’s important to distinguish these uses in the assertion
message:
assert(x.isSorted(), "Precondition failed: x is not sorted.")
That said, resist the temptation to skip a precondition check in release builds before measuring its effect on performance. The value of stopping the program before things go too far wrong is usually higher than the cost of any particular check. Certainly, any precondition check that prevents a safe function from misusing unsafe operations must never be turned off in release builds.
extension Array {
/// Exchanges the first and last elements.
mutating func swapFirstAndLast() {
precondition(!self.isEmpty)
if count() == 1 { return } // swapping would be a no-op.
withUnsafeBufferPointer { b in
f = b.baseAddress
l = f + b.count - 1
swap(&f.pointee, &l.pointee)
}
}
}
The precondition check above prevents an out-of-bounds access to a non-existent first element, and cannot be skipped without also making the function unsafe (in which case “unsafe” should appear in the function name).
What To Do When Postconditions Can’t Be Upheld
Suppose you identify a condition where your function is unable to fulfill its postconditions, even though its preconditions are satisfied. That can occur one of two ways. (These examples represent code in an unfinished state):
-
Something your function uses has a precondition that you can’t be sure would be satisfied:
extension Array { /// Returns the number of unused elements when a maximal /// number of `n`-element chunks are stored in `self`. func excessWhenFilled(withChunksOfSize n: Int) { count() % n // n == 0 would violate the precondition of % } } -
Something your function uses can itself report a runtime error:
extension Array { /// Writes a textual representation of `self` to a temporary file /// whose location is returned. func writeToTempFile(withChunksOfSize n: Int) -> URL { let r = FileManager.defaultTemporaryDirectory .appendingPathComponent(UUID().uuidString) "\(self)".write( // compile error: call can throw; error not handled to: r, atomically: false, encoding: .utf8) return r } }
In general, when a condition C is necessary for fulfilling your postcondition, there are four possible choices:
- You can strengthen the function’s signature to ensure C will be upheld.
- You can make C a precondition of your function
- You can make the function report a runtime error to its caller
- You can weaken the postcondition (e.g. by returning
Optional<T>instead ofT). 5
Strengthening the Function Signature
When C is expressible solely in terms of the function’s parameters,
you may be able to encapsulate it in a type invariant—as we did with
EmployeeDatabase in the Contracts chapter—and pass that instead. Of
course, this approach is only a win when C is required in more than
one place; otherwise it simply moves to the initializer of the new
type.
Adding a Precondition
It’s appropriate to add a precondition when:
-
It is possible for the caller to ensure C is fulfilled. In the second example above, the call to
writecan fail because the storage is full (among other reasons). Even if the caller were to measure free space before the call and find it sufficient, other processes could fill that space before the call towrite. We cannot make sufficient disk space a precondition in this case, so we should instead propagate the error:extension Array { /// Writes a textual representation of `self` to a temporary file /// whose location is returned. func writeToTempFile(withChunksOfSize n: Int) throws -> URL { let r = FileManager.defaultTemporaryDirectory .appendingPathComponent(UUID().uuidString) try "\(self)".write(to: r, atomically: false, encoding: .utf8) return r } } -
It is affordable for the caller to ensure the precondition. For example, when deserializing a data structure you might discover that the input is corrupted. The work required by a caller to check for corruption before the call is usually nearly as high as the cost of deserialization, so validity is an inappropriate precondition for deserialization. That said, remember that ensuring a precondition can often be done by construction, which makes it free. If the input is always known to be machine-generated by the same OS process that parses it, a precondition is an appropriate choice.
Reporting a Runtime Error
Swift provides two ways to report runtime errors: throwing an
Error and returning a Result<T, E: Error>. The choice of which to
use is an API design judgement call, but it is dominated by one
consequential fact:
In most cases, when a callee can’t fulfill its postconditions, neither can the caller—that inability instead propagates up the call chain to some general handler that restores the program to a state appropriate for continuing, usually after some form of error reporting.
Because this pattern is so common, most languages provide first-class features to accommodate it without causing this kind of repeated boilerplate:
let someValueOrError = thing1ThatCanFail()
guard case .success(let someValue) = someValueOrError else {
return someValueOrError
}
let otherValueOrError = thing2ThatCanFail()
guard case .success(let otherValue) = otherValueOrError else {
return otherValueOrError
}
Swift’s thrown errors fill that role by propagating errors upward with
a simple try label on an expression containing the call.
let someValue = try thing1ThatCanFail()
let otherValue = try thing2ThatCanFail()
Doing anything with the error other than propagating it requires a
much heavier do { ... } catch ... { ... } construct, which is
slightly heavier-weight than the pattern-matching boilerplate, making throwing a
worse choice when clients do not directly propagate errors.
The great ergonomic advantage of throwing in the common case means
that returning a Result only makes sense when it’s very likely that
your callers will be able to satisfy their postconditions, even when
faced with your runtime error. For example, a low-level
function that makes a single attempt to send a network packet is very
likely to be called by a higher-level function that retries several
times with an exponentially-increasing delay before failing. The
low-level function might return a Result, while the higher-level
function would throw. These cases, however, are extremely rare, and
if you have no special insight into your function’s callers, choosing
to throw is a pretty good bet.6
Dynamic Typing of Errors
The overwhelming commonality of propagation means that functions in
the call chain above the one initiating the error report seldom
depends on detailed information about thrown errors. The usual
untyped throws specification in a function signature tells most
callers everything they need to use the function correctly. In fact,
since reporting the error to a human is typically the only useful
response when propagation stops, the same often applies to the
function that ultimately catches the error: any Error provides
localizedDescription
for that purpose.
Swift does have a “typed throws” feature that lets you encode possible error types in the types of functions, but we suggest you avoid it, because it doesn’t scale well and tends to “leak” what should be an implementation detail into a function’s interface. Because failing in a new way can be a breaking change for clients that use the same feature, it adds development friction which—if overcome—causes ripples of change throughout a codebase. In languages with statically constrained error reporting, programmers routinely circumvent the mechanism because it is a poor match for common usage and has too high a cost to the development process.
You can think of a thrown error the same way you’d think of a returned
any P (where P is a protocol—Error in this case): we normally
don’t feel obliged to specify all the possible concrete types that can
inhabit a given protocol instance, because the protocol itself
provides the interface clients are expected to use. Just as an is
test or as? cast is able to interrogate the concrete type of a
protocol instance, so can a catch clause, but that ability does not
oblige a function to expose the details of those types.
Of course, an alternative to the “open” polymorphism of any P is the
“closed” polymorphism of an enum. Each has its place, but for all
the reasons outlined above, open polymorphism is generally a better
fit for the use case of error reporting.
The exception to this reasoning is once again the case where clients
are very unlikely to directly propagate the error, in which case you
are likely to use Result<T, E> rather than throwing, and using a
more specific error type than any Error might make sense.
How to Document Runtime Errors
Because a runtime error report indicates a failure to fulfill postconditions, information about errors—including that they are possible—does not belong in a function’s postcondition documentation, whose primary home is the summary sentence fragment.7
In fact, because most callers propagate errors, it’s very common that
nothing about errors needs to be documented at all: throws in the
function signature indicates that arbitrary errors can be thrown and
no further information about errors is required to use the function
correctly.
That does not mean that possible error types and conditions should never be documented. If you anticipate that clients of a function will use the details of some runtime error programmatically, it may make sense to put details in the function’s documentation. That said, resist the urge to document these details just because they “might be needed.” As with any other detail of an API, documenting errors that are irrelevant to most code creates a usability tax that is paid by everyone. Keeping runtime error information out of postconditions (and thus summary documentation) works to simplify contracts and make functions easier to use.
A useful middle ground is to describe reported errors at the module level, e.g.
Any
ThisModulefunction thatthrowsmay report aThisModule.Error.
A description like the one above does not preclude reporting other
errors, such as those thrown by a dependency like Foundation, but
calls attention to the error type introduced by ThisModule.
Documenting Mutating Functions
When a runtime error occurs partway through a mutating operation, a partially mutated state may be left behind. Trying to describe these states in detail is usually a bad idea. Apart from the fact that such descriptions can be unmanageably complex—try to document the state of an array from partway through an aborted sorting operation—it is normally information no client can use.
Partially documenting these states can be useful, however. For
example, Swift’s
sort(by:)
method guarantees that no elements are lost if an error occurs, which
can be useful in code that manages allocated resources, or that
depends for its safety on invariants being upheld (usually the
implementations of safe types with unsafe implementation details).
The following code uses that guarantee to ensure that all the
allocated buffers are eventually freed.
/// Processes each element of `xs` in an order determined by the
/// [total
/// preorder](https://en.wikipedia.org/wiki/Weak_ordering#Total_preorders)
/// `areInOrder` using a distinct 1Kb buffer for each one.
func f(_ xs: [X], orderedBy areInOrder: (X, X) throws -> Bool) rethrows
{
var buffers = xs.map { x in
(p, UnsafeMutablePointer<UInt8>.allocate(capacity: 1024)) }
defer { for _, b in buffers { b.deallocate() } }
buffers.sort { !areInOrder($1.0, $0.0) }
...
}
The strong guarantee that no mutation occurs at all in case of an error is the easiest to document and most useful special case:
/// If `shouldSwap(x, y)`, swaps `x` and `y`.
///
/// If an error is thrown there are no effects.
func swap<T>(
_ x: inout T, _ y: inout T, if shouldSwap: (T, T) throws->Bool
) rethrows {
if try shouldSwap(x, y) {
swap(&x, &y)
}
}
A few caveats about mutation guarantees when errors occur:
- Known use cases are few and rare: most allocated resources are
ultimately managed by a
deinitmethod, and uses of unsafe operations are usually encapsulated. Weigh the marginal utility of making guarantees against the complexity it adds to documentation. - Like any guarantee, they can limit your ability to change a function’s implementation without breaking clients.
- Avoid making guarantees if it has a performance cost. For example, one way to get the strong guarantee is to order operations so the first mutation occurs only after all throwing operations are complete. Some mutating operations can be arranged that way at little or no cost, but you can do it to any operation by copying the data, mutating the copy (which might fail), and finally replacing the data with the mutated copy. The problem is that the copy can be expensive and you can’t be sure all clients need it. Even when a client needs to give the same guarantee itself, your work may be wasted: when operations A and B give the strong guarantee, the operation C composed of A and then B does not (if B fails, the modifications of A remain). If you need a strong guarantee for C, another copy is required and the lower-level copies haven’t helped at all.
Weakening The Postcondition
There are several ways to weaken a postcondition. The first is to make
it conditional on some property of the function’s inputs. For
example, take the sort method from the previous chapter. Instead of
making it a precondition that the comparison is a total preorder, we
could weaken the postcondition as follows:
/// Sorts the elements so that all adjacent pairs satisfy
/// `areInOrder`, or permutes the elements in an unspecified way if
/// `areInOrder` is not a [total
/// preorder](https://en.wikipedia.org/wiki/Weak_ordering#Total_preorders)
/// .
///
/// - Complexity: at most N log N comparisons, where N is the number
/// of elements.
mutating func sort(areInOrder: (Element, Element)->Bool) { ... }
As you can see, this change makes the API more complicated to no
advantage: no client wants an unspecified permutation
from sort.8
Another approach is to intentionally expand the range of values
returned. For example, Array’s existing subscript is
declared as:
/// The `i`th element.
subscript(i: Int) -> Element
but could have instead been designed this way:
/// The `i`th element, or `nil` if there is no such element.
subscript(i: Int) -> Element?
The change adds only a small amount of complexity to the contract, but
consider the impact on callers: every existing use of array indexing
now needs to be force-unwrapped. Aside from the runtime cost of all
those tests and branches, seeing ! in the code adds cognitive
overhead for human readers. In the vast majority of callers, the
precondition of the original API is established by construction with
no special checks, but should a client need to check that an index is
in bounds, doing so is extremely cheap.
Occasionally, though, a weakened postcondition is appropriate.
Dictionary’s subscript taking a key is one example:
/// The value associated with `k`, or `nil` if there is no such value.
subscript(k: Key) -> Value?
In this case, it’s common that callers have not somehow ensured the
dictionary has a key k, and checking for the presence of the key in
the caller would have a substantial cost similar to that of the
subscript itself, so it’s much more efficient to pay that cost once in
the subscript implementation.
How to Choose?
If you can solve your problem by strengthening your function signature, that’s the best choice. It makes the function more self-documenting and leaves nothing to check (or handle) at runtime.
Failing that, whenever appropriate, you should prefer to add a precondition, because:
-
It makes it easy to identify incorrect code. A failure to satisfy the condition becomes a bug in the caller, which aids in reasoning about the source of misbehavior. If the precondition is checkable at runtime, you can even catch misuse in testing, before it becomes misbehavior.
-
Making a client deal with the possibility of return values or runtime errors that will never occur in practice forces authors and readers of client code to think about the case and the code to handle it (or about why that code isn’t needed).
-
Adding error reporting or expanded return values to a function inevitably generates code and costs some performance. Most often these results can’t be handled in the immediate caller, so are propagated upwards, spreading the cost to callers, their callers, and so forth. (The control flow implied by
tryhas a cost similar to the cost for checking and propagating a returnedResult<T, any Error>). -
The alternatives complicate APIs.
Most of the time, when a precondition isn’t added, it makes sense to report a runtime error, because it preserves the idea of the function’s simple primary purpose, implying that all the other cases are some kind of failure to achieve that purpose. Weakening the postcondition means considering more cases successful, which makes a function into a multipurpose tool, which is usually harder to document, use, and understand.
Clearly weakening a postcondition seldom pays off and should be used
rarely. If you must weaken the postcondition, returning an Optional<T>
instead of a T adds the least possible amount of information to the
success case, and thus does the least harm to API simplicity. It can
be appropriate when there will never be a useful distinction among
reasons that the function can’t produce a T. Subscripting a
Dictionary with its key type is a good example. The only reason it
would not produce a value is if the key were not present.
Lastly, remember that the choice is in your hands, and what you choose has a profound effect on clients of your code. There is no criterion that tells us a condition must or must not be a runtime error other than the effect it has on client code.
Handling Runtime Errors Correctly
The previous section was about how to design APIs; this one covers how to account for errors in function bodies.
Reporting or propagating an Error From a Function
When a function exits with an error, either locally initiated or
propagated, any resources such as open files or raw memory allocations
that are not otherwise managed must be released. The best way to
manage that is with a defer block releasing the resources
immediately after they are allocated:
let f = try FileHandle(forReadingFrom: p)
defer { f.close() }
// use f
If the resources must be released somewhere other than the end of the
scope where they were allocated, you can tie them to the deinit of
some type:
struct OpenFileHandle: ~Copyable {
/// The underlying type with unmanaged close functionality
private let raw: FileHandle
/// An instance for reading from p.
init(forReadingFrom p: URL) { raw = .init(forReadingFrom: p) }
deinit {
raw.close()
}
}
When Propagation Stops
Code that stops propagation of an error and continues has one fundamental obligation: to discard any partially-mutated state that can affect the behavior of your code (which excludes log files, for example). In general, this state is completely unspecified and there’s no other valid thing you can do with it. Use of a partially mutated instance other than for deinitialization is a bug.
For the same reasons that the strong guarantee does not compose, neither does the discarding of partial mutations: if the second of two composed operations fails, modifications made by the first remain. So ultimately, that means responsibility for discarding partial mutations tends to propagate all the way to the top of an application.
In most cases, the only acceptable behavior at that point is to present an error report to the user and leave their data unchanged, i.e. the program must provide the strong guarantee. That in turn means—unless the data is all in a transactional database—a program must usually follow the formula already given for the strong guarantee: mutate a copy of the user’s data and replace the data only when mutation succeeds.9
Let It Flow
The fact that all partially-mutated state must be discarded has one profound implication for invariants: when an error occurs, with two rare exceptions detailed below, a mutating method need not restore invariants it has broken, and can simply propagate the error to its caller. Allowing type invariants to remain broken when a runtime error occurs may seem to conflict with the very idea of an invariant, but remember, the obligation to discard partially mutated state implies that only incorrect code can ever observe this broken state.
Why Not Maintain Invariants Always?
The most obvious advantage of the “let it flow” approach over trying
to keep invariants intact is that it simplifies writing and reasoning
about error handling. For most types, discardability is trivial to
maintain, but invariants often have more complex relationships. A
less obvious advantage is that in some cases, it allows stronger
invariants. For example, imagine a disk-backed version of PairArray
from the last chapter, where I/O operations can throw:
/// A disk-backed series of `(X, Y)` pairs, where the `X`s and `Y`s
/// are stored in separate files.
struct DiskBackedPairArray<X, Y> {
// Invariant: `xs.count == ys.count`
/// The first part of each element.
private var xs = DiskBackedArray()
/// The second part of each element.
private var ys = DiskBackedArray()
// ...
/// Adds `e` to the end.
public mutating func append(_ e: (X, Y)) throws {
try xs.append(e.0) // breaks invariant
try ys.append(e.1) // restores invariant
}
}
All mutations of a DiskBackedArray perform file I/O and thus can
throw. In the the append method, if ys.append(e.1) throws,
there may be no way to restore the invariant that xs and ys have
the same length. If the rule were that invariants must be maintained
even in the face of errors, it would force us to weaken the invariant
of DiskBackedPairArray.
The Exceptions: Invariants That Must Be Maintained
The first exception to the “let it flow” rule is for invariants
depended on by a deinit method—the ones that maintain
discardability. However, deinit methods are rare, and deinit
methods with dependencies on invariants that might be left broken in
case of an error are rarer still. You might encounter one in a
ManagedBuffer subclass—see the Data Structures chapter for more
details.
The second exception for invariants of types whose safe operations are
implemented in terms of unsafe ones. Any invariants depended on to
satisfy preconditions of those unsafe operations must of course be
upheld to maintain the safety guarantees. So, for example, if a
supposedly-safe operation deallocates an UnsafePointer, it depends
on the precondition that the pointer was returned by an earlier
allocation and hasn’t been deallocated. Any invariant that ensures the
precondition would be satisfied (e.g. “p: UnsafePointer<T>? is
either nil or valid for deallocation”) must be upheld by all
mutating methods.
The key to controlling any invariant is to factor the properties
involved into a struct whose only job is to manage the values of
those properties, and keep write access to those properties private.
Establish the invariant in this struct’s init methods, and—for these
exceptional cases—take care that it is restored before propagating any
errors from its mutating methods.
Conclusion
This chapter completes the Better Code picture of how to program by contract. Your key takeaways:
- Programming errors (bugs) are mistakes in the program code. The most effective response to bug detection is to terminate the program.
- Runtime errors signal dynamic conditions that prevent fulfilling postconditions, even when all code is correct.
- Most runtime errors are propagated to callers.
- To keep contracts simple and a function’s primary purpose clear, and to emphasize the information most clients need, keep documentation about errors out of summaries and postconditions. Consider omitting detailed error information altogether, or documenting it only at the module level.
- To keep invariants strong and simple and to reduce the mental tax of
handling errors that propagate, do not try to maintain invariants
(except those depended on for
deinitmethods or safety) when mutating operations fail. - To make designs easy to evolve with low friction, resist the temptation to represent the static types of errors in function signatures.
-
While some folks like to use the word “error” to refer only to what we call runtime errors—as the authors have done in the past—the use of “error” to encompass both categories seems to be the most widespread practice. We’ve adopted that usage to avoid clashing with common understanding. ↩
-
There do exist systems that recover from bugs in a principled way by using redundancy: for example, functionality could be written three different ways by separate teams, and run in separate processes that “vote” on results. In any case, the loser needs to be terminated to flush any corrupted program state. ↩
-
Techniques for ensuring that restarting is seamless, such as saving incremental backup files, are well-known, but outside the scope of this book. ↩
-
Actually, if you build your program with
-Onone, both forms have no effect; the conditional expression will never even be evaluated. However,-Ononemakes Swift an unsafe language: any failure to satisfy preconditions can cause undefined behavior. The results can be so serious that we strongly advise against using-Onone, except as an experiment to satisfy yourself that Swift’s built-in checks do not have unacceptable cost. The rest of this book is therefore written as though-Ononedoes not exist. ↩ -
Most functions that return
Optional<T>, and what Swift calls a “failable initializer” (declared asinit?(…)) can be thought of as taking a “weakened postcondition” approach. Despite the name “failable initializer,” by our definition anilresult represents not a runtime error, but a successful fulfillment of the weakened postcondition. ↩ -
Returning a
Resultcould also make sense when most callers are going to transform the error somehow before propagating it, but code that propagates transformed errors is also very rare. The use cases forResultare rare enough, in fact, that it’s a reasonable choice to alwaysthrowfor runtime error reporting. ↩ -
This rule creates a slightly awkward special case for functions that return a
Result<T,E>, which should be documented as though they just return aT:
↩extension Array { /// Writes a textual representation of `self` to a temporary file, /// returning its location. func writeToTempFile(withChunksOfSize n: Int) -> Result<URL,IOError> { ... } } -
We’ve seen attempts to randomly shuffle elements using
x.sort { Bool.random() }, but that has worse performance than a properx.randomShuffle()would, and is not guaranteed to preserve the same randomness properties. Perhaps more importantly, the code lies by claiming to sort when it in fact does not. ↩ -
This pattern is only reasonably efficient when the data is small or in a persistent data structure. Because of Swift’s use of copy-on-write for variable-sized data, any data structure built out of standard collections can be viewed as persistent provided none are allowed to grow too large, but easier and more rigorous implementations of persistence can be found in swift-collections, e.g.
TreeSetandTreeDictionary↩
Types
Types are a way of assigning meaning to raw bits. Consider this type:
/// A point in the [Cartesian
/// plane](https://en.wikipedia.org/wiki/Cartesian_coordinate_system#Cartesian_plane).
struct Point2D {
/// The first coordinate (abcissa).
public var x: Float
/// The second coordinate (ordinate).
public var y: Float
}
Point2D represents more than just (Float, Float)—or even (x: Float, y: Float)—does, because of the meaning implied by its
documentation.
Value
Every instance has a notional value, expressed in terms of the
operations on its type. 64 bits can be used to represent an integer, a
finite set (by using each bit as a Bool denoting membership), a
short string (as UTF-8), etc., but in ``1 as UInt64the bits represent the mathematical integer value 1. We know it's a mathematical integer not only from the documentation, but also from the available operations—such as addition—on the instance, and their meanings. AlthoughUInt64` has the operations necessary for
treating instances as finite sets, doing so requires assigning a layer
of additional meaning to operations such as division and modulus.
Note: every instance of a type should have exactly one clearly-defined value. 1
Equality
The fact that instances have values implies they can be compared for equality: at the risk of stating the obvious, two instances are equal when the have the same value.
Range
We call set of (non-negative) mathematical integers the notional
range of Int64. At a high level, we program as though types can
represent all members of their notional range, but because memory is
finite and for performance reasons, a type’s range—the set of
values it can represent—is often a subset of its notional range.
Types can take different approaches to this discrepancy. In Swift, integer arithmetic operations have the precondition that the result is representable, and violating that precondition is a fatal error. Floating point operations, on the other hand, give approximate results. Approximation is very difficult to specify and work with, so a representability precondition is almost always the preferred approach.
The value of a Point2D
instance is directly represented by the values of its stored x and
y properties.
Fundamental Value Operations
Because instances have values, we can
Graph
Let’s examine a more interesting case:
/// A [directed simple
/// graph](https://en.wikipedia.org/wiki/Graph_(discrete_mathematics)#Directed_graph).
struct DirectedGraph {
/// A location that may be connected to other locations via a
/// directed edge.
public typealias Vertex = Int
/// For each vertex u, the target vertices of edges originating at u.
private var successors_: [Vertex: Set<Vertex>] = [:]
/// The vertex that will be added by the next call to makeVertex().
private var nextVertex: Vertex = 0
/// Returns a new vertex.
public mutating func makeVertex() -> Vertex {
defer { nextVertex += 1 }
successors_[nextVertex] = []
return nextVertex
}
/// Adds an edge from `source` to `target`.
public mutating func addEdge(from source: Vertex, to target: Vertex) {
precondition(successors_.index(forKey: source) != nil)
precondition(successors_.index(forKey: target) != nil)
successors_[source]!.insert(target)
}
/// All the vertices.
public var vertices: some Collection<Vertex> {
successors_.keys
}
/// The vertices reachable from `v` in one step.
public func successors(_ v: Vertex) -> some Collection<Vertex> {
successors_[v]!
}
}
The value of a DirectedGraph is captured in the identities of its
vertices and the edges between them. While all that information is
present in its successors_ property, it is not directly represented, which
is why successors_ is private.
Meaning => Operations
The meaning of a type implies a set of operations. For example,
extension Point2D {
/// Returns the length of the line segment between `self` and
/// `other`.
public func distance(to other: Point2D) -> Float {
(x * other.x + y * other.y).squareRoot()
}
}
Basis Operations
The minimal set of operations required to implement all other
meaningful operations on a type is called its complete basis. The
complete basis of Point2D is simply provided by read/write access
to its stored properties. The minimal set of operations required to
implement all others with maximal efficiency on a type is called its
efficient basis.
- Array of pairs efficient basis
Copy, Equality, Hash, etc.
And their relationships
- Copying
- Equality
- Hashing
- Comparison
- Assignment
- Serialization
- Differentiation
- Regular
Problems With References
Spooky action
Incidental algorithms
Visibly broken invariants
Race conditions
Surprise mutation
Unspecifiable mutation
Beware Mutable Captures
The whole-part relationship
Where ~Copyable Fits
Representing Graphs
Copy On Write, ManagedBuffer, et. al.
Misc
- Value
- Types can be viewed as sets of values
- Point
- Set
- Notional value often not representable.
- Bool is an outlier
- Finite sets in general
- Float
- Composite Types
- Reachability of Parts (Equational Completeness) Set
- How to Compromise Notional Value (limits of finite representations)
- Non-salient properties: floating point -0, +0
- NaN, domain of operations (outside of value set - doesn’t obey equality laws)
- Type invariants mention
-
Sets shouldn’t be collections
-
non-salient properties
-
points, vectors, pairs
-
strong type information and engineering tradeoffs
-
documenting types
-
types and naming
-
when to expose properties (computed or stored).
-
subscripts
-
law of exclusivity
-
basis operations
- completeness - implement equality
- efficiency
-
Arguably,
Intand friends do not follow this rule because bitwise operations directly offer an interpretation of the value as a sequence of bits. To clarify the value, these operations could have been made available only on some type that can be constructed from theInt. That, however, would break strong precedent from other languages, which is arguably more important. ↩