Skip to content
academia.sh

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.

Start typing to search.

↑↓ Esc navigate · open · close