The first programmable devices, street organs and Jacquard weaving looms, appeared in Europe during the 18th century. Both were controlled by punched cards. The presence or absence of holes in the card determines which notes are played and which threads are selected. The cards are divided into lines that are executed sequentially, one after the other. As shown in figure 0.1, often the cards form a loop, with the end of the last card being affixed to the beginning of the first card, so that the musical tune or the visual pattern repeats indefinitely.
![]()
Figure 0.1: Jacquard control unit for a weaving loom, with its punched cards arranged in a loop. (CC BY-NC-SA, Heinz Nixdorf MuseumsForum.)
I like to think that the notions of sequence and loop in computer programming stem directly from this handling of punched cards in organs and looms. In any case, these two notions have played a crucial role in the modern computer — the programmable electronic calculator whose program is stored in memory — ever since its appearance in the mid-1940s. Even today, machine-code programs still consist of processor instructions stored in memory and executed sequentially, one after the other, except for branch or jump instructions, which cause execution to continue at a specified code address. (See figure 0.2 for an example.)
Figure 0.2: x86 machine code that computes Fibonacci numbers and sends them to output port 32. The flow of control is shown by the arrows on the left side. The jump at PC 15 to PC 10 creates an infinite loop.
The first programming languages (assembly languages, autocoders, Fortran I, …) expose a machine-style view of control: base instructions are executed sequentially; unconditional and conditional jumps, like the famous goto command, allow computations to be repeated or skipped. By the end of the 1950s, the limitations of this approach had become apparent, prompting subsequent programming languages, such as Algol 60 and its many descendants, to introduce higher-level control structures:
Meanwhile, other programming paradigms emerged, such as functional programming (Lisp, 1960) and logic programming (Prolog, 1972). These languages leave control mostly implicit: source code focuses on defining which values or predicates to compute, rather than how to compute them. However, control is still present in these languages, only under different forms: in the evaluation and resolution strategies used for program execution; and, in the case of functional languages, in their ability to represent control flows as function values called continuations, and to program their own custom control structures as library functions that manipulate continuations.
This book is a journey through the design space and historical time of programming languages, guided by the notions and paradigms of control. We will attempt to answer questions such as:
Our approach is mainly descriptive, often comparative, and sometimes formal, but never prescriptive. It spans the timeline from 1947, when the first assembly language was developed, to 2022, when effect handlers were first integrated into a mainstream programming language. It also spans the spectrum from programming examples to programming language theory.
The remainder of this book is divided in four parts.
Part I: Control structures for imperative languages describes, compares, and shows the historical evolution of control structures for conventional imperative and object-oriented languages.
Part II: Control operators for functional languages is devoted to functional programming languages and their ability to manipulate control flows as data called continuations, which makes it possible to define custom control structures as libraries.
Part III: From exceptions and monads to algebraic effects and handlers describes a recent development in the area of control operators for functional languages, effect handlers, and their underlying theory, algebraic effects.
Part IV: reasoning about control and effects shows how to establish safety and correctness properties about control and effects, using type systems and program logics.
Readers are assumed to be familiar with one imperative programming language, such as C, C++, Java or Python, and one functional programming language, such as Haskell, OCaml, Scheme or SML. The companion website XXX collects the code examples given in this book and provides instructions for executing them. The more technical chapters also assume some knowledge of programming language theory, which can be found in the electronic textbook Programming Languages Foundations (Pierce et al., 2025), for example.
There are several ways to read this book. Readers who are mainly interested in the comparison between programming languages and their historical evolution can focus on chapters 1 to 5 and 8 to 10. Those interested in functional programming and the its uses of continuations can skip part I and focus on chapters 5 to 10. Those who are familiar with some of this material but want to learn more about recent developments around algebraic effects can start at part III and focus on chapters 9 to 15. Finally, readers interested in programming language foundations and their use to support program verification will enjoy chapters 6, 8, and 11 to 15.
The level of technical difficulty does not increase linearly throughout the book. Rather, it increases within each part, and often within each chapter. Readers who find a passage too technical are encouraged to skip to the next chapter or even to the next part.