Lesson 15 / 18
Structural Programming
Building programs with sequence, selection, and iteration; abandoning the jump statement and top-down decomposition.
Contents
The structures used throughout this course — sequential statements, conditional branching, loops, and functions — belong to a particular organizing philosophy. That philosophy is called structural programming, and its looking natural does not mean it was accepted without dispute; it is the outcome of a debate.
This topic compares three organizing approaches. Each answers differently the question of what units a program is divided into and how those units communicate. All three can solve the same problem; their differences show up as programs grow.
Three Structures Suffice
The core claim of structural programming is this: every computable program can be written using only three control structures.
- Sequence: Statements executed one after another.
- Selection: Following one of two paths depending on a condition.
- Iteration: Repeating a block depending on a condition.
That these three suffice is a formally proven result. Its practical meaning is that no fourth structure — in particular, a statement that jumps to an arbitrary point in the program — is needed.
The Problem with the Jump Statement
Early languages included a statement that allowed jumping directly to a desired line of the program. At the hardware level this is natural: as seen in the previous course, a branch instruction is nothing more than writing a new address into the program counter.
The problem shows up in readability. In a program written with unrestricted jumps, which paths can reach a given line cannot be determined by reading the text. Reasoning about the value of a variable at that point requires examining every path that reaches it, and the number of paths grows quickly with the number of jumps.
Structural control structures solve this problem by definition: each structure has exactly one entry and one exit. A block is entered only at its start and left only at its end. This constraint makes it possible to understand what a block does by looking at the block itself.
Early-exit statements — breaking out of a loop, returning early from a function — are a controlled relaxation of this constraint. What sets them apart from unrestricted jumping is that their targets are fixed and structural: an exit always goes to the end of the structure it is inside.
Top-Down Decomposition
The second component of structural programming is breaking the problem down step by step. The process starts at the top: the program is broken into a few high-level steps; each step is broken into its own substeps; the breaking stops once each piece is small enough to be written directly.
The decomposition in the pseudocode lesson follows exactly this method. For the running problem:
summarize(measurements) ├── validate(measurements) → empty?, valid? ├── add_up(measurements) → total ├── find_largest(measurements) → largest └── average(total, count) → average
Each node corresponds to a procedure. The top-level procedure knows what the lower-level
ones do; it does not know how they do it. This information hiding limits how far a
change propagates: when the inside of find_largest changes, its caller is unaffected.
The Structural Solution to the Common Problem
def validate(measurements: list[int]) -> None: """Checks that the measurement list is processable.""" if not measurements: raise ValueError("measurement list is empty") def add_up(measurements: list[int]) -> int: total = 0 for measurement in measurements: total += measurement return total def find_largest(measurements: list[int]) -> int: largest = measurements[0] for measurement in measurements: if measurement > largest: largest = measurement return largest def summarize(measurements: list[int]) -> tuple[float, int]: """Returns the average and the largest value.""" validate(measurements) return add_up(measurements) / len(measurements), find_largest(measurements) print(summarize([12, 18, 7, 25, 14])) # (15.2, 25)
The structure of the solution matches the decomposition tree exactly. Each procedure does one thing, can be tested on its own, and its name says what it does.
Grouping Procedures
As decomposition continues, the number of procedures grows into a flat list. A second organizing layer is needed: related procedures are collected into the same module.
A module is the practical counterpart of the namespace defined in the scope lesson. Procedures that work with measurement data sit in one module, reporting procedures in another. A module boundary does two things: it separates names, and it documents which procedures change together.
A module also has an interface. Procedures exposed to the outside are separated from helpers used only internally; this separation is the same internal-versus-external visibility distinction from the previous course’s binding lesson. Exposing few procedures to the outside makes changing the inside of a module easier.
This organization is what keeps structural programming workable in large programs: the program is no longer read as a hierarchy of procedures but as a relationship between modules. The scaled-up version of the same idea — components and layers — is the subject of the Software Architecture curriculum.
Data and Procedures Kept Separate
The distinguishing feature of this approach is that data and the procedures that
operate on it are defined separately. The measurements list is a data structure;
add_up and find_largest are independent procedures that operate on it. Data travels
to procedures as an argument.
In small programs this arrangement is clear and needs no extra concept. As a program grows, two difficulties appear.
First, which procedures a data structure can be used with is written nowhere in the code; the relationship rests on convention alone. Passing the same list to the wrong procedure is an error the language does not prevent.
Second, the rules of a data structure — its invariants — are preserved in a scattered way. The rule “the measurement list is never empty” has to be re-checked in every procedure that uses the list; when one procedure forgets, the rule is silently violated.
These two difficulties are the starting point of the next lesson: the idea of gathering data and the behavior that operates on it into a single unit.
The Testability Gain
A less-discussed but decisive consequence of decomposition is testability. A single long procedure can only be tested by running it from start to finish; decomposed procedures can each be tested independently.
In the solution above, find_largest can be tested without reading a file, writing to
the screen, or depending on any other step: a list is given, the returned value is
compared against the expected one. Edge cases — a single-element list, all equal values
— are tested with the same ease.
This gain grows when procedures are pure: a procedure that looks at nothing beyond its input and leaves no trace outside requires no setup to test. The same observation becomes the core idea of a paradigm in this topic’s third lesson.
Where Structural Programming Stands
Structural programming was not invalidated by the paradigms that followed; it became the ground they were built on. The method of an object-oriented class and the function of a functional language are both written, on the inside, with sequence, selection, and iteration.
For this reason, the question of “which paradigm” is asked not at the level of control structures but at the level of what units a program is divided into. The structural answer is the procedure.
Summary
- Structural programming holds that every program can be written with sequence, selection, and iteration; these three suffice.
- Unrestricted jumping makes it impossible to trace which paths reach a given point; structural control structures have a single entry and a single exit.
- Early-exit statements are a controlled relaxation of this constraint because their target is fixed.
- Top-down decomposition breaks a problem into procedures; the top-level procedure knows what the lower level does, not how it does it.
- Keeping data and procedures separate is simple in small programs, but in large programs it makes preserving the relationship and the invariants harder.
- Decomposed procedures can be tested independently; related procedures are grouped into modules to establish name separation and a boundary for change.
Next Step
Gathering data and the procedures that operate on it into a single unit answers directly the two difficulties that emerged at the end of this lesson. The next lesson introduces the basic concepts of the object-oriented approach and solves the same problem again, this time through an object.
To keep your progress and take notes, Log in
My notes
Log in to take notes.