o be a better programmer, write little proofs in your head
Train yourself to sketch small, continuous proofs about your code to design faster, more accurate software and measure its "proof-affinity."
- Core idea: As you write code, sketch "little proofs" in your head that it does what you intend—online, without breaking flow—to reduce iterations and errors.
- Monotonicity in code: Favor processes that only move forward (e.g., checkpointing) to rule out broad classes of failure; immutability is a close cousin that simplifies reasoning by preventing state rollback.
- Database analogies: LSM trees (mostly additive, compacting later) are often easier to reason about than in-place structures like B-trees due to monotonic progression of state.
- Pre- and post-conditions: Explicitly state what must be true before and after a function runs; use them to derive unit tests and add assertions for defensive correctness and simpler mental models.
- Invariants: Identify properties that must hold before, during, and after execution; prove each atomic step preserves the invariant, ensuring consistency regardless of execution order or failures.
- Lifecycle enforcement: Use constructors/destructors (C++) or hooks (e.g., React’s useEffect) to maintain invariants at critical moments and keep multiple states in sync.
- Isolation and "firewalls": Changes have a blast radius; design boundaries that stop propagation so you can prove unaffected behavior remains intact—conceptually related to encapsulation and the Open-Closed Principle.
- Example (Nerve query engine): Instead of changing planner and executor internals to handle virtual-field dependencies, over-pull dependencies during planning and prune after execution, confining change to two layers and preserving untouched "meat" of the pipeline.
- Induction for recursion: Reason about recursive structures and functions incrementally—prove base case, then inductive step assuming the hypothesis holds for substructures.
- Concrete induction example: A tree-simplification function contracts nodes by grafting children upward; assuming subtrees are already simplified, only parent-child contraction remains, yielding a fully contracted result.
- Proof-affinity (dual perspective): Design code so it’s easy to prove properties about it:
- Write monotonic, immutable flows where possible.
- Start from clear pre-/post-conditions and structure code around them.
- Subdivide into minimal units that individually preserve invariants.
- Build strong component boundaries to limit change propagation.
- Implement recursive functions incrementally, separating base and inductive cases.
- Quality metric: If it’s easy to prove your code is correct, the design is likely strong; persistent difficulty suggests refactoring to increase proof-affinity.
- Practice to build intuition: Make micro-reasoning instinctive through deliberate practice—write mathematical proofs, do exercises (not just read).
- Recommended practice paths: Stanford’s algorithms course on EdX for proof-oriented thinking; selective LeetCode problems to exercise correctness reasoning (avoid "trick" problems, focus on minimal tries to a correct submission).
- Outcome: Better clarity, fewer regressions, faster iteration, and a design ethos that treats provability as a catalyst for good programming.
The full post is available here.