Reading Code as Problem-Solving: Decoding Intent from Implementation
What This Concept Is
When you read someone else's code -- to fix a bug, review a change, or onboard into a project -- you are not doing a different activity from writing code. You are solving a problem. The problem is:
Given this artifact, reconstruct the intent it was written to satisfy, and decide whether it satisfies it.
An author's code is a partially-justified solution: the code is the output, but the reasoning that produced it is mostly missing. Variable names, comments, and tests are hints, not the argument. You have to reverse-engineer the argument.
The same intellectual move appears in three settings:
- reading a proof someone else wrote -- reconstructing why each line follows
- reading code -- reconstructing the invariants the author had in mind
- reading a system in production -- reconstructing the implicit protocol between components
All three require the solver to hold a piece of code or text in one hand and a hypothesis in the other, and iterate until the two match.
Reading code as problem-solving differs from writing code in one crucial respect: the direction is reversed. Writing code goes from intent -> implementation. Reading code goes from implementation -> intent. The techniques therefore flip: you look for evidence of intent (invariants, termination measures, boundary cases) rather than generating them.
This concept is the dual of concept 13: writing a spec turns informal intent into formal intent; reading code turns formal implementation back into (inferred) formal intent -- and if the two specs disagree, you have found either a bug or a documentation gap.
Why It Matters Here
Most code you will ever work with was written by someone else, often a past version of you who no longer remembers the reasoning. At a job, reading code is more common than writing new code. The ability to read efficiently is a compounding skill.
In particular:
- bug triage is a reading problem before it is a writing problem
- code review must understand the change well enough to approve or reject
- onboarding into a codebase is nine-tenths reading
- production incidents require reading unfamiliar code under time pressure
Module 5's debugging concept (see concept 11) assumed your code, your reasoning. Cluster 5 extends that skill to someone else's code, where the reasoning is not yours and has to be rebuilt from evidence.
Forward pipeline: Semester 2 (reading canonical algorithm implementations as the fastest way to learn them), Semester 3 (code review and refactoring depend on reading skill), Semester 4 (reading OS and kernel code to diagnose systems behaviour), Semester 7 (reading across architectural boundaries), Semester 10 (capstone onboarding and incident response).
Concrete Examples
Example 1 -- what does this mystery function do?
You open a file and see this loop. No comments, no docstring, no tests.
def mystery(a):
lo, hi = 0, len(a) - 1
best = None
while lo <= hi:
mid = (lo + hi) // 2
if a[mid] >= 0 and (best is None or a[mid] < a[best]):
best = mid
if a[mid] >= 0:
hi = mid - 1
else:
lo = mid + 1
return best
Apply the reading protocol.
- Boundary tracing. On
a = [],lo = 0,hi = -1, loop does not execute, returnsNone. Handled. - Invariant hunt. Inside the loop,
best, if set, is an index of a non-negative value. After assignment,a[best]is strictly less than any previousa[best]. Sobesttracks the minimum non-negative value seen so far, at the smallest index that achieves it. - Search direction. If the middle element is non-negative, the code moves
hileft. If negative, movesloright. The author assumed the array is sorted ascending with some negatives first and non-negatives after -- a "find the first non-negative" pattern. On a general unsorted array, this would not work. - Small-input simulation. Try
a = [-3, -1, 0, 2, 5]. Loop correctly lands on index 2, value 0. Trya = [-3, -1]. ReturnsNone. Trya = [0, 2, 5]. Returns index 0. Hypothesis consistent. - Termination measure.
hi - lodecreases by at least 1 each iteration; the loop terminates. - Conclusion. The function performs a binary search for the first non-negative element in a sorted array; it returns that index or
Noneif all elements are negative.
You reconstructed intent from the implementation without a single comment. That is the skill.
Example 2 -- reading a proof
The same move works on paper. You read in a textbook:
Theorem. For every graph $G$ on $n \ge 2$ vertices, some two vertices have the same degree. Proof. The degree of any vertex is in ${0, 1, \dots, n-1}$. But a vertex of degree $0$ and a vertex of degree $n-1$ cannot both exist. So the degrees lie in a set of size $n-1$. By pigeonhole, two vertices share a degree. $\square$
Treat this as code to read.
- Frame the question. What result is being proved? Same-degree pair exists.
- Identify the shape. Pigeonhole with a size argument. Look for the "boxes" and the "items".
- Find the invariant. "$\deg(v) \in {0, 1, \dots, n-1}$" -- a range.
- Find the load-bearing trick. "A vertex of degree $0$ and a vertex of degree $n-1$ cannot both exist." Why not? A degree-$(n-1)$ vertex is adjacent to every other vertex, contradicting a degree-$0$ vertex's isolation. This line does the work; it shrinks the range from $n$ options to $n-1$.
- Small-case check. $n = 2$: two vertices, each degree 0 or 1; they must match. $n = 3$: one vertex has degree 2 ⇒ the other two have degree $\ge 1$ ⇒ any pair satisfies the statement. Hypothesis survives.
- Conclusion. The proof is correct; the key step is the mutual exclusion between degree-$0$ and degree-$(n-1)$ vertices.
Same protocol, different artifact: reading is reading.
Common Confusion / Misconceptions
Reading linearly. Starting at line 1 and going to line $N$. This only works for short functions. For anything non-trivial, jump to the core loop or core branch and work outward.
Trusting names. Variable names like best, result, flag lie under pressure. The name was chosen when the code was clearer; it may no longer reflect current behaviour. Verify with boundary cases, not with names.
Skipping the small-input test. Simulating the function on $n = 0, 1, 2$ is the fastest way to falsify a reading hypothesis. Learners skip it because it feels slow; it is actually much faster than staring.
Rewriting instead of understanding. The urge to "clean up" code before understanding it is a way of avoiding the reading problem. The refactor may be correct -- or it may destroy an invariant the author relied on. Read first, refactor second.
How To Use It
Code-reading protocol:
- Frame the question. What do you need this reading to answer? "Is there a bug in how it handles empty input?" calls for different reading depth than "why is this slow?".
- Identify the shape. Loop, recursion, state machine, pipeline, visitor? The shape tells you what invariants to look for.
- Pin the boundaries. What happens at the start, the end, the empty case, the one-element case?
- Find the invariant. Inside every loop or recursion, something stays true. Name it. Write it in the margin.
- Find the termination measure. What quantity is strictly decreasing (or strictly increasing toward a bound)? If you cannot name it, the code may not terminate on some input.
- Hypothesis-test cycle. State what you think it does. Run a small input through in your head or a debugger. If output matches, strengthen the hypothesis; if not, revise.
- Label the bug, if there is one. Use the three-category taxonomy from concept 11: model / plan / execution.
The same protocol, with "code" replaced by "proof", is how a mathematician reads a proof. Same intellectual habit; different artifact.
Transfer / Where This Shows Up Later
- Semester 2 (algorithms). Reading reference implementations of algorithms you just learned is the fastest way to cement them; the protocol here transfers directly.
- Semester 3 (refactoring & design patterns). Effective refactoring starts with reading: name the invariant, then transform the code in a way that preserves it. Pattern catalogues are reading aids.
- Semester 4 (systems programming). Reading kernel source, syscall handlers, and device drivers is a standard skill; the loop-invariant/termination-measure habit survives the move to C.
- Semester 5 (protocols). Reading a protocol spec is reading code in a different notation; the "find the invariant" move becomes "find the state-machine invariant".
- Semester 7 (architecture). Reading existing architecture -- service boundaries, module graphs -- is the review skill for ADR proposals.
- Semester 10 (capstone). On-call rotations and production incidents reward reading-under-pressure: the boundaries, the invariant, the termination measure, in that order.
Check Yourself
- Why is "read the function top to bottom" usually a bad reading strategy for non-trivial code?
- What two artifacts should you pull out of any non-trivial loop or recursion?
- Why is simulating on $n = 0, 1, 2$ a high-leverage move even when you think you already understand the code?
- How does the code-reading protocol apply to reading a proof? Give the one-to-one mapping.
- You read a function and decide it is wrong. How do you classify the bug before proposing a fix?
Mini Drill or Application
Drill A. Pick a non-trivial function (30-60 lines) from a codebase you did not write. Without running it, produce:
- a one-paragraph summary of what it does
- the invariant of the main loop or recursion, written as a sentence
- the termination measure
- a table with $n = 0, 1, 2$, the expected output, and the observed output when you actually run it
- a labelled classification of any discrepancy: model / plan / execution bug, or "no bug; hypothesis was wrong."
Then read the tests or docstring. Where was your hypothesis wrong, and why?
Drill B. Take a short proof (half a page) from a textbook and apply the reading protocol. Identify the "shape" (induction / pigeonhole / contradiction / construction), the invariant, and the load-bearing step.
Drill C (unseen). Given this function, state what it does (in one sentence), its invariant, and its termination measure:
def f(a):
i, j = 0, len(a) - 1
while i < j:
if a[i] + a[j] < 0:
i += 1
else:
j -= 1
return i
Assume a is sorted ascending. If the function has a bug on an edge case, name it.
Read This Only If Stuck
- Concept 3: Carrying Out the Plan Requires Per-Step Verification
- Concept 11: Debugging Mathematical and Algorithmic Reasoning Is Its Own Skill
- Dromey: 1.4.4 Debugging programs -- bug taxonomy applied to someone else's code
- Dromey: 1.5 Program verification -- invariants as the primary reading artifact
- Dromey: 1.5.5 Verification of program segments with branches -- branch-by-branch reading
- Dromey: 1.5.8 Proof of termination -- the termination measure as a reading aid
- MCS: 6.2 The invariant principle -- invariants as the shared vocabulary of proofs and programs
- External: Polya, How to Solve It (Wikipedia summary) -- "have you seen it before?" applied to reading someone else's solution