Step 1 of 4 · Reading · ~3 min
Learn
Real-World Python
Putting It All Together
You will be able to design the state of a command-driven program so a command can be undone or cancelled: decide when a snapshot is pushed, popped and cleared, and check that the stack stays consistent from one command to the next by tracing it by hand. The other layers plug in unchanged: errors (inheritance lesson), the registry (decorators), the input generator (generators) and json.dumps(..., sort_keys=True) output.
A ledger with named holds
Every failure is a subclass of one base, caught in one place in the loop (except LedgerError as err: print(err.message)). The state class owns the data and a stack of snapshots, holds: pairs of a label and an earlier copy of the balances.
release abandons the changes since the latest hold; settle keeps the changes and closes only the latest hold; rewind LABEL abandons everything since the hold called LABEL. With no open hold, release and settle raise NothingHeld. Start with credit ann 10:
| command | stack afterwards | balances |
|---|---|---|
hold a | [a:{ann: 10}] | {ann: 10} |
credit ann 5 | [a:{ann: 10}] | {ann: 15} |
hold b | [a:{ann: 10}, b:{ann: 15}] | {ann: 15} |
debit ann 12 | [a:{ann: 10}, b:{ann: 15}] | {ann: 3} |
settle | [a:{ann: 10}] | {ann: 3} |
hold c | [a:{ann: 10}, c:{ann: 3}] | {ann: 3} |
credit ann 1 | [a:{ann: 10}, c:{ann: 3}] | {ann: 4} |
rewind a | [] | {ann: 10} |
release | [] | error, balances unchanged |
Reading the trace
Read the stack column, not the balances. Changes never touch the stack; only hold, release, settle and rewind do, and a snapshot is never edited after it is pushed. That is the invariant: the stack holds exactly one snapshot per open hold, each the state at the moment its hold opened. Whatever happens inside later holds, such as the settle on row 5, a still remembers {ann: 10}, which is why rewind a can undo changes that an inner settle had already kept.
Designing a command means asking what it does to the stack, what it does to the balances, and whether it can fail before changing either. Change one rule, say a settle that closes every hold, and the same input gives a different trace: tracing is how you test a rule before writing it.
Why this shape
Exceptions rather than return codes, because one forgotten check makes a failed command look successful. Snapshots rather than inverse operations, because an inverse per command is where undo bugs breed.
Common mistakes
- Appending the live dict,
self.holds.append((label, self.balances)): the "snapshot" changes with the books, so aftercredit ann 10,hold a,credit ann 5,releasethe balance is 15, not 10. - Treating
settleas "close everything": afterhold a,hold b,settle, areleaseshould still restorea's snapshot but raisesnothing heldinstead. - Changing before validating: if
debitsubtracts first,debit ann 80leaves-70behind although it printederror: overdrawn. - Copying one level deep:
dict(a)shares inner lists. Strings and numbers are safe; list values needcopy.deepcopy.
Your exercise
Build an in-memory key-value store with nested transactions, using the same four layers: an error family, a state class with a snapshot stack, a registry, and one catch in the loop. The ledger's rules are not the exercise's rules, so trace the stack for the example before coding and decide what each of the three transaction commands does to it.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…