Part 1: Foundations of Software Construction
Software exists to solve real problems. These chapters are about building solutions that work, and using a language that helps you get them right.
You already know how to program, and you already know that programs can go wrong. Part 1 builds on that groundwork and lays the foundations of software construction that apply across a broad set of programming languages.
We will cover two concerns across Part 1. The first concern is capability: the building blocks a real program needs, from structured data to collections, changing state, and communication with files and services. You know many building blocks already. The new ones signal a shift to imperative programming. We will see how far these building blocks can go in Chapter 4, which will motivate the introduction of object-oriented programming in Part 2. Along the way we will learn TypeScript, a language that can do more than the teaching languages in CPSC 110 and checks far more of your work.
The second concern is correctness: specifying what a program should do precisely enough that the language, the tests, or you can confirm that it does. In CPSC 110 you learned to defend against mistakes through discipline: you documented each function's signature, you wrote examples before the function bodies, and you followed design recipes carefully. That discipline worked, but almost none of it was enforced. A signature that said (@signature Number -> String) was a promise you made to yourself, but the language did not check it. TypeScript will allow us to enforce many more correctness properties. We will be careful to separate three kinds of assurance: guarantees the compiler can check before the program runs, behaviours that can only be confirmed by running it, and promises that still rest on the discipline of the programmer.
Our programs in Part 1 stay small enough that one person can hold the whole design in their head. That assumption is what allows personal discipline to uphold the promises the language cannot. Part 2 moves beyond this size restriction and asks what happens when programs, teams, and lifetimes outgrow any one person.
Intended Learning Objectives
By the end of Part 1, you will be able to:
- Model information as precise types, designing data definitions whose structure drives the code that operates on them.
- Specify behaviour with contracts and invariants, and construct tests that target the cases most likely to reveal faults, then judge whether a suite adequately covers them.
- Decide how each property should be confirmed, whether by the type system, by tests, or by controlling how values are created, and design code that enforces the invariants the language cannot check and reports the failures it cannot prevent.
- Reason about state and time, tracing how references, scope, and mutation determine what a change affects, and weighing the trade-offs of mutation, side effects, and asynchronous computation.
- Build working programs that combine these ideas to process collections and interact with files and web services.
Layered Correctness
A type communicates intent: a well-designed type tells the next reader exactly which values are valid, and the compiler enforces that intent before the program runs. But types describe structure, and many correctness properties are about meaning. A balance must stay non-negative, a course grade must sit between 0 and 100, and a binary search tree must keep its keys in order. A value can have exactly the right shape and still be meaningless. The properties that must hold beyond the types are called invariants, and the assumptions and guarantees a function documents, its preconditions and postconditions, are its contract.
Identifying these properties and confirming them are separate tasks. The separation follows the boundary between the static and dynamic views of a program. The static view is the source text, which the compiler can analyse without running it. That is where types are checked. The dynamic view is the program as it runs, taking on actual values and following particular paths. Whether it computes the right answer is a dynamic question, and can only be answered by running the code. These mechanisms form layers, and each guards against a failure the others cannot see:
- Types establish what shapes of data are allowed.
- Contracts state what behaviour each function assumes and promises.
- Invariants state what must remain true of the data at all times.
- Tests execute the code, choosing which inputs and paths to exercise.
- Assertions check that the code's behaviour matches what was expected.
No layer is sufficient on its own. A program can be correctly typed and still compute the wrong answer. A contract can be precisely worded and still be violated. A test suite can pass while another part of the program produces invalid data. A passing test is evidence about the particular runs you tried, while a type check or a maintained invariant is a claim about every run. Knowing which kind of assurance you actually have is an important part of engineering.
How Guarantees Fail
Five common failure modes break guarantees, and each corresponds to a missing layer:
- Over-trusting types: assuming that type-correct means semantically correct.
- Vague contracts: wording like "valid" or "correct" with no explicit criteria, leaving a promise no one can check.
- Unowned representation: exposing the raw shape of the data so clients can bypass the safe operations.
- Happy-path tests: checking typical outputs but never whether the invariants survive more diverse operations.
- Weak assertions: confirming only that a call returned something, without checking that the behaviour was correct.
When you find a bug that "should have been impossible," ask which of these five is responsible.
One concern remains. An invariant the language cannot check must still be kept true, and this requires control over creation. If any code can build a value, every such place is an opportunity to break the invariant. Chapter 4 uses encapsulation to hide values using only the language features you already know from CPSC 110. In Part 2, we learn about a new language feature that provides encapsulation more directly, but the idea is the same as in Chapter 4: protecting an invariant shapes how the code is organised.
Chapter Overview
Part 1 covers four broad themes across nine chapters.
The language and its data:
- Learning a New Programming Language introduces TypeScript from Intermediate Student Language: types as a checked mechanism, the compiler, statements like
ifandreturn, and the static and dynamic views the rest of the part builds on. - Using Types to Model Problems designs precise data: compound types, unions for distinct cases, and recursive structure, with functions whose shape follows the shape of the data.
Correctness:
- Checking Invariants records contracts and invariants, derives tests from them using equivalence classes and boundary values, and reports erroneous outcomes with a result type.
- Maintaining Invariants keeps an invariant true for the life of a program by controlling creation with a constructor function and hiding state inside a closure.
The capabilities of real programs:
- Arrays and Iteration introduces collections and the operations over them:
map,filter,reduce, andfind, with thefor ofloop beneath them. - Mutation and Side Effects adds state that changes over time, along with the references, aliasing, scope, and side effects that come with it.
- Asynchronous Effects and Time reaches outside the program to files and web services, where a result arrives only after a wait, using promises and
async/await, and exchanges data with them as JSON.
Handling and verifying failure:
- Designing for Failure treats failure as part of a function's contract, choosing between returning a failure the type checker forces callers to confront and throwing an exception that propagates to a handler above.
- Validating Behaviour moves from the course toolkit to the assertion vocabulary of a real test framework, partitions inputs and outputs, and uses coverage and regression to judge whether a suite checks enough.
Toward Part 2: Abstraction
Part 1 ends with promises the language cannot check and a technique for keeping these promises through disciplined design. Part 2 moves that approach into the code itself: classes bundle data together with the operations allowed on it, and encapsulation puts the representation out of reach so an invariant cannot be broken from outside. From there it builds the vocabulary for abstractions that others can depend on: interfaces that state a contract, implementations that can stand in for one another, and designs that stay open to extension as requirements change.