The same problem,
ninety years apart
Every program on this page does one thing: total the amounts by department and print a report. The problem never changes. What changes is what a programmer thought a program was — a wiring diagram, a deck of cards, a stack, a matrix, a set of facts, a sentence.
Each exhibit runs its own execution model in the console beneath it, because these machines do not merely spell one algorithm differently. They hold different things in their hands while they work.
Read down and something odd happens. The 1935 machine and the 1974 database express the same idea, and the forty years in between are spent writing by hand what both of them got for free.
The deck
12 cards · 24 columns punched · input to all elevenA payroll register from a firm that never existed, staffed entirely by people who did. Twelve 80-column cards, punched in three fields: name in columns 1–10, department in 11–18, amount in 19–24 with an implied decimal. Every exhibit below reads exactly this.
Presorted on the department field — four of the eleven programs below simply will not work otherwise. Four departments, twelve records, grand total 1341.65. Departments: ASSEMBLY · PAYROLL · SHIPPING · TOOLROOM.
There is no program
A program is a shape you wire into a panel. It has no first step, because it happens to every card at once.
* 407 CONTROL PANEL — MINOR TOTALS ON DEPT
* DECK MUST FIRST PASS THE 082 SORTER ON COLS 11-18
FROM TO
READ EXIT 1-10 NAME -> PRINT ENTRY 1-10
READ EXIT 11-18 DEPT -> PRINT ENTRY 20-27
READ EXIT 11-18 DEPT -> COMPARING ENTRY B
READ EXIT 19-24 AMT -> COUNTER 4A ADD
COMPARING EXIT UNEQUAL -> PROGRAM START (MINOR)
COUNTER 4A TOTAL EXIT -> PRINT ENTRY 30-38
COUNTER 4A TOTAL EXIT -> COUNTER 4A RESETThere is no source listing for this exhibit because there is no source. The behaviour of the machine is the set of wires in the panel, and to change the report you unplug a cord and plug it somewhere else. A 407 program was a physical object you carried to the machine room in a frame.
The whole report hangs on one cord: COMPARING EXIT UNEQUAL to PROGRAM START. The 407 compares each card’s department field against the one before it, and when they differ it fires a control break — take the counter’s accumulated total, print it, clear the counter, resume.
That comparison only works because the deck arrived sorted, and the sort is not part of the program at all. It happened on a different machine, in a different room, possibly on a different day. The precondition lives in the world rather than in the code, which is the single most alien thing about this exhibit.
— carriage at home, no output —Write the break out longhand
A program is a sequence of statements with numbers on them, and the numbers are where you go.
C TOTAL BY DEPARTMENT
C CARDS PRESORTED ON DEPT CODE, COLS 11-12
SUM = 0.0
GRAND = 0.0
LAST = 0
DO 40 J = 1, 12
READ 100, N1, N2, KODE, AMT
IF (LAST) 10, 10, 20
10 LAST = KODE
20 IF (KODE - LAST) 30, 35, 30
30 PRINT 200, LAST, SUM
GRAND = GRAND + SUM
SUM = 0.0
LAST = KODE
35 SUM = SUM + AMT
40 CONTINUE
PRINT 200, LAST, SUM
GRAND = GRAND + SUM
PRINT 300, GRAND
STOP
100 FORMAT (2A4, I2, F7.2)
200 FORMAT (1H , A8, F9.2)
300 FORMAT (1H0, 11HGRAND TOTAL, F9.2)
ENDEverything the 407 did structurally, FORTRAN makes you say. The comparison, the print, the reset, the resume — four cords become nine statements, and the statement numbers are the only thing holding them together.
IF (KODE - LAST) 30, 35, 30 is the arithmetic IF: evaluate the expression, then branch to one of three labels depending on whether it came out negative, zero or positive. It is a three-way GOTO, and it was the primary conditional in the language.
Note that the department is an integer code rather than a name. FORTRAN II had no character type; text existed only as Hollerith constants inside FORMAT statements, which is why the literal 11HGRAND TOTAL has to carry its own length as a prefix. The data had to be numbered before it could be processed, and somebody in the building kept the codebook.
And there at the bottom: the duplicated PRINT 200 after the loop. The last department has no successor to differ from, so its total never breaks. Every control-break program written between 1957 and roughly 1985 contains this same small act of remembering, and the ones that forgot silently dropped a department.
— carriage at home, no output —The report is a data declaration
A program is a description of records, and then some sentences about them.
DATA DIVISION.
FILE SECTION.
FD PAY-FILE LABEL RECORDS ARE OMITTED.
01 PAY-CARD.
05 PC-NAME PIC X(10).
05 PC-DEPT PIC X(8).
05 PC-AMOUNT PIC 9(4)V99.
WORKING-STORAGE SECTION.
77 WS-PREV-DEPT PIC X(8) VALUE SPACES.
77 WS-DEPT-TOTAL PIC 9(6)V99 VALUE ZERO.
77 WS-GRAND-TOTAL PIC 9(7)V99 VALUE ZERO.
01 RPT-LINE.
05 RL-DEPT PIC X(14).
05 RL-TOTAL PIC ZZZ,ZZ9.99.
PROCEDURE DIVISION.
MAIN-LINE.
PERFORM READ-A-CARD.
PERFORM PROCESS-CARD UNTIL END-OF-FILE.
PERFORM PRINT-BREAK.
MOVE 'GRAND TOTAL' TO RL-DEPT.
MOVE WS-GRAND-TOTAL TO RL-TOTAL.
WRITE RPT-LINE AFTER ADVANCING 2 LINES.
STOP RUN.
PROCESS-CARD.
IF PC-DEPT NOT = WS-PREV-DEPT
AND WS-PREV-DEPT NOT = SPACES
PERFORM PRINT-BREAK.
MOVE PC-DEPT TO WS-PREV-DEPT.
ADD PC-AMOUNT TO WS-DEPT-TOTAL.
PERFORM READ-A-CARD.
PRINT-BREAK.
MOVE WS-PREV-DEPT TO RL-DEPT.
MOVE WS-DEPT-TOTAL TO RL-TOTAL.
WRITE RPT-LINE.
ADD WS-DEPT-TOTAL TO WS-GRAND-TOTAL.
MOVE ZERO TO WS-DEPT-TOTAL.The control break is identical to FORTRAN’s — same comparison, same reset, same forgotten-final-group trap. What COBOL adds is somewhere to put the layout.
PIC ZZZ,ZZ9.99 is an edited picture, and it is the most underrated idea in the language: it declares that this field, when something is moved into it, will suppress leading zeros, insert a comma at the thousands and fix two decimals. The formatting is a property of the destination, not an act of the code. Modern printf is a step backwards from this.
COBOL is mocked for its verbosity, but look at what the verbosity buys. A person who does payroll and not programming can read ADD PC-AMOUNT TO WS-DEPT-TOTAL and audit it. That was the explicit design goal, and by that measure it worked for sixty years.
— carriage at home, no output —The loop you are not allowed to write
A program is a set of specifications, and the machine supplies the control flow.
H* TOTAL BY DEPARTMENT RPG II
FPAYCARD IP F 24 READ01 SYSIPT
FREPORT O F 80 PRINTER
*
* I-SPEC: FIELD DESCRIPTIONS. THE "L1" IS THE WHOLE TRICK.
IPAYCARD AA 01
I 1 10 NAME
I 11 18 DEPT L1
I 19 24 2AMT
*
* C-SPEC: THE ONLY LINES YOU ACTUALLY WROTE
C 01 AMT ADD DTOT DTOT 72
C L1 DTOT ADD GTOT GTOT 82
*
* O-SPEC: WHAT PRINTS, AND ON WHICH INDICATOR
OREPORT T L1
O DEPT 20
O DTOT J 38
OREPORT T LR
O 'GRAND TOTAL' 20
O GTOT J 38Look for the loop. There is not one. RPG has a fixed program cycle built into the runtime: read a card, set indicators, do total-time output, run the calculation specs, do detail output, repeat until the hopper is empty. You do not write it and you cannot change it.
The two characters L1 in columns 59–60 of the input spec declare that DEPT is a control field at level one. From that single annotation the cycle derives everything — it saves the previous value, compares on every card, sets the L1 indicator when it changes, and runs total-time output before the new card’s detail processing. The control break has become a declaration again, twenty-nine years after the 407.
LR is last-record, set automatically at end of file, which means RPG cannot have FORTRAN’s dropped-final-group bug: the last break is not something a programmer is able to forget.
The price is total. Your program has one shape, and programs that do not fit the cycle get written by fighting it; generations of RPG programmers have the scars. But for this problem — and this problem was most of commercial computing in 1964 — six lines do the work of COBOL’s forty.
— carriage at home, no output —The grouping is a matrix
A program is an expression. Control structure dissolves into the shape of the data.
⍝ DEPT is a 12-row character matrix, AMT a 12-vector
⍝ U is the four distinct departments
U ← ∪ DEPT
T ← (U ∘.≡ DEPT) +.× AMT
⍝ and that is the program.
⍝ ∘.≡ builds a 4×12 boolean membership matrix
⍝ +.× is the inner product: rows against amounts
⍝ the grand total, for completeness:
+/TThe entire report is one line, and it contains no loop, no accumulator, no comparison against a previous value, and no dependence whatsoever on the order of the deck.
U ∘.≡ DEPT is an outer product: take every department name against every card’s department and produce a matrix of ones and zeros. That matrix is the grouping — row two is a mask selecting the payroll cards. Watch it build in the panel below.
Then +.×, the inner product, multiplies that matrix by the amount vector: exactly matrix multiplication, which means each row contributes an amount only where its mask is one. Grouping and summing turn out to be the same operation as linear algebra, and once you have seen that you cannot unsee it.
This is a different claim from every exhibit above it. FORTRAN and COBOL and RPG all agree that a program is a sequence of things happening to one card at a time. APL says the whole deck is a single value, and the program is a function of it.
| B | I | R | C | H | H | A | K | K | W | K | M | |
| ASSEMBLY | ||||||||||||
| PAYROLL | ||||||||||||
| SHIPPING | ||||||||||||
| TOOLROOM |
— carriage at home, no output —The stack is the whole machine
A program is a vocabulary. You extend the language downward until the problem is a word in it.
VARIABLE SUM VARIABLE GRAND VARIABLE LAST
: BREAK LAST @ .DEPT SUM @ .MONEY CR
SUM @ GRAND +! 0 SUM ! ;
: RECORD SWAP DUP LAST @ <> IF
LAST @ -1 <> IF BREAK THEN
LAST !
ELSE DROP THEN
SUM +! ;
: RUN 12 0 DO I CARD RECORD LOOP
BREAK ." GRAND TOTAL" GRAND @ .MONEY CR ;This one genuinely executes. The panel below is a small token-threaded Forth running that exact source a word at a time, so you can watch the data stack fill and drain. Amounts are integer cents, because Forth of this era had no floating point and did not miss it.
There are no parameters and no local variables anywhere. RECORD receives its arguments because CARD left them on the stack, and it juggles them with SWAP and DUP. This is either liberating or appalling depending on the hour of the day.
The real idea is the colon. Every definition adds a word to the dictionary, and from that moment it is indistinguishable from a built-in — BREAK is now part of the language. You do not write a program in Forth so much as grow a language down towards the problem, which is why the top-level word is three lines long.
The control break is back to being hand-written, as it was in FORTRAN. But notice where it lives now: it is a named thing, defined once, and the last line reads almost like the RPG spec sheet two exhibits up.
— carriage at home, no output —Say what a total is, not how to get one
A program is a set of true statements. Running it means asking a question.
% the deck, as facts. amounts in cents.
pay(backus, assembly, 12675).
pay(iverson, assembly, 10930).
pay(rossum, assembly, 8815).
pay(codd, payroll, 9760).
pay(hollerith, payroll, 11840).
pay(hopper, payroll, 13425).
pay(aho, shipping, 10420).
pay(kay, shipping, 9985).
pay(kernighan, shipping, 11195).
pay(weinberger, shipping, 9370).
pay(kowalski, toolroom, 11550).
pay(moore, toolroom, 14200).
% a department total is the sum of the amounts in it.
total(Dept, Total) :-
setof(D, N^A^pay(N, D, A), Depts),
member(Dept, Depts),
findall(A, pay(_, Dept, A), Amounts),
sum_list(Amounts, Total).
% the report is every solution to that.
?- forall(total(D, T), report_line(D, T)).Nothing here describes a procedure. total(Dept, Total) is a claim about when two things stand in a relation, and the report is produced by asking for every pair that satisfies it. The engine does the searching.
findall(A, pay(_, Dept, A), Amounts) reads: collect every A such that somebody in this department was paid it. The underscore is an anonymous variable — the name is irrelevant to the question, so it does not get one.
Deck order is meaningless here, as it was for APL, but for a different reason. APL made order irrelevant by treating the deck as one value; Prolog makes it irrelevant by treating it as an unordered set of assertions. Both had to abandon the card feed to get there.
And the same program answers questions it was never written for. ?- total(shipping, T). asks about one department. ?- pay(N, D, A), A > 12000. asks who earns most. There is no main. There is a database of truths, and whatever you care to ask of it.
— carriage at home, no output —Name the thing the 407 was wired for
A program is a description of the answer. Someone else decides how to get it.
SELECT dept,
SUM(amount) AS total
FROM payroll
GROUP BY dept
ORDER BY dept;Four lines, and every one of them is a cord from the first exhibit. ORDER BY is the 082 sorter. GROUP BY is the comparing exit. SUM is counter 4A. The reset is so thoroughly implied that the language has no word for it.
This is the hinge of the whole page. Thirty-nine years after the accounting machine, and after four exhibits of programmers writing the sort and the break and the reset out by hand, somebody wrote the four ideas down as names and the problem stopped being a program.
What changed underneath is the interesting part. The 407 could only do this because a human had already sorted the deck; the query planner decides for itself whether to sort-and-break or build a hash table, and on a large enough table it picks the plugboard’s strategy for the plugboard’s reasons. Sorting in order to group never stopped being the right answer — it stopped being the programmer’s problem.
Codd’s claim in 1970 was that data has a structure independent of how any particular program wants to walk it. Nothing on this page before this exhibit believed that. Everything after it assumes it.
| Bucket | Accumulated |
|---|---|
| empty — nothing read yet | |
— carriage at home, no output —The program cycle, with a Unix accent
A program is a set of rules: when you see this, do that. The reading is not your job.
{ total[$2] += $3 }
END { for (d in total)
printf "%-14s%9.2f\n", d, total[d]
printf "%-14s%9.2f\n", "GRAND TOTAL", sum }Two rules. The first has no condition, so it runs on every line; the second runs at end of input. There is no loop in the source because awk supplies one — read a record, split it into fields, test each pattern, run the actions that match, repeat. Exactly as RPG did it in 1964.
total[$2] += $3 creates the bucket on first touch. Associative arrays were not a common language feature in 1977, and they are what let the grouping be one character of syntax instead of a data structure you have to build.
Compare this with the RPG spec sheet three exhibits up. Both declare per-record behaviour and let the runtime own the sequence; both have an automatic end-of-input hook, called END here and LR there. Two entirely separate traditions independently rediscovered the accounting machine’s shape, one in commercial data processing and one inside a research phone company.
And unlike every card-era exhibit, the deck order is irrelevant — because the hash table remembers what the control break had to be told.
| Bucket | Accumulated |
|---|---|
| empty — nothing read yet | |
— carriage at home, no output —There is no file to run
A program is a live world of objects. You do not run it; you talk to it while it is already running.
| totals |
totals := deck
inject: Dictionary new
into: [:acc :card |
acc at: card dept
put: (acc at: card dept ifAbsent: [0]) + card amount.
acc].
totals keys asSortedCollection do: [:d |
Transcript show: (d paddedTo: 14);
show: ((totals at: d) / 100) printString;
cr].
"or, once you have taught the collection the idea:"
(deck groupedBy: [:card | card dept])
collect: [:group | group inject: 0 into: [:a :c | a + c amount]]You do not compile this. You select it inside a running image, press do it, and it happens to objects that were already alive — and that stay alive afterwards. The deck is not read from a file at the start of a run, because there is no start of a run.
[:acc :card | … ] is a block: an object that holds code, which you hand to the collection so that it can call you back. Every conditional and every loop in the language is built from this and nothing else. ifTrue: is a message sent to a boolean with a block as its argument, and that is not a metaphor or a teaching simplification.
The last stanza is the Smalltalk move. groupedBy: is not a language feature — it is a method somebody added to Collection one afternoon, inside the image, while the system was running, and from then on it was as native as anything Xerox shipped. This is the same instinct as Forth’s dictionary, ten years later and a great deal larger.
The consequence nobody expects: an image can be saved with your half-finished work still on the stack, mailed to a colleague, and resumed mid-thought on their machine. The program and its execution are the same artefact.
| Bucket | Accumulated |
|---|---|
| empty — nothing read yet | |
— carriage at home, no output —Everything, and nothing to see
A program is one line. What it does underneath is not a question you are expected to ask.
import pandas as pd
df = pd.read_fwf("payroll.txt",
colspecs=[(0,10),(10,18),(18,24)],
names=["name","dept","amount"])
print(df.groupby("dept")["amount"].sum())One line does the work. It is a good line, it is the right line, and almost nobody who writes it could tell you what happens inside.
What happens inside is a hash aggregate — and if the frame is large enough, or the operation spills to disk, what happens inside is a sort followed by a control break, because that is still, in 2026, the correct algorithm when the groups do not fit in memory. The 407 was not doing a primitive version of this. It was doing this.
Look at colspecs=[(0,10),(10,18),(18,24)]. Those are the punched card’s column boundaries, and the function is called read_fwf — read fixed width formatted. Ninety years on, the deck is still a supported input format, and the columns have not moved.
This is not a lament. Layers that hide their machinery are how anything large gets built, and nobody should have to wire a panel to add up a column of numbers. But it is worth knowing that the thing being hidden is not complexity for its own sake. It is a sorter, a comparator and a counter, and somebody wired them together before the war.
| Bucket | Accumulated |
|---|---|
| empty — nothing read yet | |
— carriage at home, no output —Put the first exhibit next to the eighth. In 1935 you sorted the deck on an 082, then wired a comparing exit to a counter’s total exit so that a change in the department field would print a subtotal and clear the counter. In 1974 you wrote four lines.
compare DEPT ≠ DEPT
counter 4A total exit
counter 4A reset
GROUP BY dept
SUM(amount)
—
These are the same four thoughts. The accounting machine expressed them as topology and the database expressed them as a name, and in both cases the programmer never writes the loop. The loop is the machine’s business.
Everything between them — FORTRAN, COBOL, the whole imperative middle — is people writing the sort and the break out longhand, because the machines of that era had learned to be general and forgotten how to be specific.
We did not go from imperative to declarative
The usual story is a ladder: machine code, then assembly, then procedures, then objects, then at long last the declarative styles, each rung an improvement on the last. Read this page from the top and that story falls apart.
We started declarative. We had to — the earliest machines were not machines you instructed, they were machines you configured, and a configuration has no time in it. A plugboard says what is true of every card. So does GROUP BY. So does an awk pattern, a Prolog clause, an APL matrix product.
Then the stored-program computer arrived and made everything possible, which meant it made nothing easy. For forty years the price of generality was that you wrote the loop, the comparison, the accumulator and the reset yourself, on every job, forever, and called the ones you got wrong bugs.
What SQL and awk and pandas recovered was not a new idea. It was the accounting machine’s idea, rebuilt on top of a general computer instead of underneath one — specificity bought back at the cost of an interpreter.
The control break was never lost, exactly. It was just, for a while, something you had to remember to do.