Foundations to FrontierPart II: FoundationsBezhig

C01: Programs as precise state changes

5 exercises, each with a complete solution. 1 figure. 3 code listings.

On this page
  1. A. Prerequisites and observable goals
  2. B. The central question: what changes, and what must remain true?
  3. C. Notation, values, names, and types
  4. D. An execution trace that explains the result
  5. E. Decomposition and method selection
  6. F. Aliasing: two names need not mean two objects
  7. G. Independent practice
  8. H. Complete solutions: C01
  9. I. Retention and readiness

A. Prerequisites and observable goals#

Before

Arithmetic, function inputs, and willingness to trace one line at a time. No calculus is required.

After

Translate a small specification into state, predict a loop’s behavior, distinguish assignment from equality, and test an implementation without confusing a successful example with a proof.

Level: Foundation; accelerate only after an unaided trace and implementation.

B. The central question: what changes, and what must remain true?#

A program is not merely a collection of commands that happens to produce a useful screen. It is a description of permitted computations. Begin with an observable contract: given a sequence of signed integer changes, return a new sequence containing the running total after each change. The input [2,-1,3] should produce [2,1,4]. An empty input should produce an empty output. The input must not be changed. A decision about invalid inputs belongs in the specification, not in a last-minute exception after a test fails.

The mathematical function maps one finite sequence to another. An implementation chooses representations, local variables, an iteration order, and error behavior. Two implementations can realize the same function while taking different amounts of time or memory. Conversely, two implementations may agree on the three examples you tried but disagree on a fourth input. Describe the contract before writing the mechanism.

C. Notation, values, names, and types#

An expression is evaluated to obtain a value: 2 + 3 produces the integer 5. An assignment such as total = total + value first evaluates the right-hand side using the current bindings, then binds the left-hand name to the result. It is not the algebraic equation t=t+vt=t+v. In Python, == tests equality, whereas = performs assignment. A Boolean is a truth value, not an English sentence.

A type determines which operations are defined and what they mean. Python integers can grow beyond a fixed machine word, subject to available memory; C++ integer types ordinarily have bounded ranges. The language difference will matter in C09. A list is an ordered, mutable sequence of references to objects. The name values refers to a list; the name is not itself the list. Strings such as "2" contain characters and are not interchangeable with the integer 2. Conversion must have a specified interpretation and can fail. These are language semantics, not matters of coding taste. [PY]

D. An execution trace that explains the result#

Python
values = [2, -1, 3]
total = 0
out = []
for value in values:
    total = total + value
    out.append(total)
MomentvalueOld totalNew totalout
Before loopnot yet bound—0[]
After first iteration202[2]
After second iteration−1-121[2,1]
After third iteration314[2,1,4]

The loop’s useful invariant is: after processing the first kk inputs, total equals their sum, and out contains exactly their kk prefix sums. Initially k=0k=0: the empty sum is zero and the empty result has length zero. One iteration adds precisely the next input and appends precisely the new sum, so the statement remains true. After nn iterations, the invariant is the desired postcondition. Since a finite list contributes one item per iteration, the loop terminates. This is already a small correctness argument; induction in C05 makes its structure explicit.

Putting total = 0 inside the loop destroys the accumulated state. Appending before adding produces totals for the wrong boundaries. Returning from inside the loop prematurely stops after one item. These mistakes are best located by asking which invariant first becomes false, not by randomly editing syntax until an example works.

E. Decomposition and method selection#

Separate validation, computation, and presentation. Validation decides whether input satisfies the contract. Computation returns a value without printing it. Presentation converts that value to text, JSON, a graph, or a user interface. This separation lets tests call the computational function without pretending to be a terminal user. For this lab, returning a list is clearer than printing a stream of partial results. A streaming interface is a legitimate later extension when inputs are too large to keep in memory, but it changes the interface and testing obligations.

Use a loop when the next state follows regularly from the current state. Use an explicit state table when order matters. Before execution, predict at least one ordinary result and one boundary result. Then compare the actual result with the prediction. An exception gives evidence about where execution stopped; it does not tell you automatically whether the input, contract, or implementation is wrong.

F. Aliasing: two names need not mean two objects#

Python
a = [1, 2]
b = a
b.append(3)
c = a.copy()
c[0] = 9

After the first mutation, both a and b refer to [1,2,3]. The copy creates a different outer list. Thus the final lists are a == [1,2,3] and c == [9,2,3]. This is a shallow copy: nested mutable objects would still be shared. Predict this from the object graph rather than memorizing that “Python passes by reference,” a phrase that obscures rebinding versus mutation.

Schematic object graph, not physical memory addresses. Arrows represent name bindings.

G. Independent practice#

F foundation C standard course work T transfer S optional stretch L runnable lab or project. Attempt the complete set before opening its solutions.

C01.01FSolution

Trace total=1, followed by a loop over [3,0,-2] that executes total=2*total+value. Give the state after every iteration. Explain the difference between assignment and equality.

C01.02CSolution

A proposed prefix-sum implementation sets total=0 at the beginning of every iteration. Predict its output for [2,-1,3], identify the first violated invariant, and repair the design without running it.

C01.03CSolution

Explain the final objects after a=[[1],[2]]; b=a.copy(); b[0].append(9). Does copying the outer list protect every nested object? Draw the relevant arrows.

C01.04TSolution

Specify a function that computes the average of latency measurements. Decide what an empty sequence, a negative measurement, a nonnumeric value, and the distinction between seconds and milliseconds should mean. Give four concrete test cases. There is more than one defensible contract; make yours consistent.

C01.05LabSolution

Implement prefix_sums(values) in the starter trace_lab.py. Accept a list of exact Python integers, reject Boolean or noninteger elements with TypeError, return a new list, and leave the input unchanged. Run the prefix tests in tests/test_labs.py. Explain the loop invariant and the time and output-space cost.

Optional small hints

C01.02: record what is retained between iterations. C01.03: distinguish the outer container from each inner list. C01.05: write down the empty-input state before entering the loop.

H. Complete solutions: C01#

Compare the first step where your work differs. A corrected answer is not yet an independent success.

C01.01Show solution

Evaluate first; assign second

Starting at 1, the first right-hand expression is 2(1)+3=52(1)+3=5, so the new total is 5. Next 2(5)+0=102(5)+0=10. Finally 2(10)−2=182(10)-2=18. The old value remains available while the right-hand expression is evaluated. The assignment then replaces the binding. The equation t=2t+3t=2t+3 would instead ask for a number satisfying an algebraic relation, namely t=−3t=-3; that is not what the statement computes.

Back to C01.01
C01.02Show solution

The reset loses the past

The faulty output is [2,-1,3] because every iteration adds its input to zero. After the first iteration the answer happens to be correct. At the beginning of the second, resetting the sum discards the previously processed 2, so the invariant fails before the addition. Move initialization outside the loop. Retest with an empty list, one element, negative changes, and cancellation such as [5,-5]. The point is to preserve accumulated state, not merely to get 4 at the end of one example.

Back to C01.02
C01.03Show solution

The inner list remains shared

There are two outer lists, but each contains references to the same two inner lists. Appending 9 mutates the first inner list, so both outer lists display [[1,9],[2]]. An assignment to b[0] would replace one outer-list reference instead; it would not replace a[0]. A deliberate deep copy or immutable nested representation can avoid the shared mutation, but each changes cost or behavior and should follow the intended contract.

Back to C01.03
C01.04Show solution

An exemplar contract

One coherent design accepts a nonempty sequence of finite, nonnegative real-valued measurements in milliseconds, rejects Boolean/nonnumeric values with TypeError, rejects an empty sequence, negative values, infinity, or NaN with ValueError, and returns a floating-point mean in milliseconds. Cases: [10,20]↦15[10,20]\mapsto15 ms; [0]↦0[0]\mapsto0 ms; [][] raises; [10,−1][10,-1] raises. Add a type case such as [10,"20"][10,\texttt{"20"}] and a nonfinite case before relying on the interface. Seconds must be converted explicitly: 0.0200.020 s is 20 ms, not 0.0200.020 ms. Returning None for empty input is another defensible contract if every caller handles it; silently returning zero would confuse missing data with measured zero latency.

Back to C01.04
C01.05Show solution

Reference implementation and correctness

code/solutions/trace_lab.pyPython
"""C01.05: prefix sums with an explicit state invariant."""
def prefix_sums(values: list[int]) -> list[int]:
    """Return a new list; reject non-integers (including bool)."""
    out: list[int] = []
    total = 0
    for value in values:
        if type(value) is not int:
            raise TypeError("all inputs must be integers")
        total += value
        out.append(total)
    return out

if __name__ == "__main__":
    print(prefix_sums([2, -1, 3]))

The input validation deliberately uses type(value) is int: Python Booleans are integer subclasses, but this exercise’s contract excludes them. Each valid input contributes one addition and one append. Under a unit-cost arithmetic approximation, time is Θ(n)\Theta(n) and output storage is Θ(n)\Theta(n). For arbitrarily large Python integers, addition also depends on the number of bits; the unit-cost model has a stated boundary. The loop invariant proved above yields the result for every finite valid list, while the tests check selected ordinary, empty, rejection, and nonmutation cases. A test passing is not the invariant’s proof.

Back to C01.05

I. Retention and readiness#

Retrieve function input versus output from Module 02 and the difference between == and a limiting statement from Module 06. Advance when you can trace a changed recurrence, explain shallow copying with a diagram, and implement the prefix-sum contract from a blank file. A working game subsystem does not waive this small independent demonstration; an excellent demonstration does waive repetitive beginner syntax drills.