Reactive Budget Notebook With Live Formulas and Cycle Rejection
OpenAISoftware Engineer · OnsiteLast updated
Build a small reactive calculator where named variables hold integers or formulas that reference other variables, values stay correct as inputs change, and any definition that would create a circular dependency is rejected. Tests graph modeling, cache invalidation, cycle detection, and writing tests live.
You are building the engine behind a personal-budget notebook. Each line defines a variable:
rent = 1200
power = 90
bills = = rent + power + 15
left = = 2000 - bills
Implement:
class Notebook:
def define(self, name: str, expr: str) -> None: ...
def value(self, name: str) -> int: ...
Expression grammar. Either an integer literal like "1200", or "=" followed by terms joined with + or -. A term is a non-negative integer or a variable name ([a-z_][a-z0-9_]*), optionally preceded by a unary -. Whitespace is ignored.
Semantics
valueof a variable that was never defined is0.- Redefining a variable replaces its formula; everything that depends on it, directly or through other variables, must reflect the change.
- If a
definewould create a circular dependency (including self-reference), raiseCycleError(a subclass ofValueError) and leave the notebook exactly as it was. - A syntax error raises
ValueErrorand also changes nothing.
Part 1: Values and propagation
Make define and value work. Reads are far more frequent than writes, so avoid recomputing unchanged values. State the cost of each operation.
Part 2: Cycle rejection
Detect cycles before committing a write. Explain why your check is sufficient.
Part 3: Tests
Write a compact test suite that would convince a reviewer. Cover at least diamond-shaped dependencies, rewiring a formula so it no longer depends on something, undefined references, and rejected writes leaving no trace.
Example
define rent 1200; define power 90
define bills "= rent + power + 15"; define left "= 2000 - bills"
value("left") -> 695
define power 140
value("bills"), value("left") -> 1355, 645
define rent "= left - 5" -> CycleError
value("rent"), value("left") -> 1200, 645
Hints
Hint 1
Store the formula separately from the computed value. Keep both edge directions: what a variable reads, and who reads it.
Hint 2
Before the write the graph is acyclic, so any new cycle must pass through the variable being defined. Check whether any new reference can reach it.
Hint 3
On a write, clear cached values for the variable and everything reachable through 'read-by' edges; recompute lazily on the next read.
Hint 4
Do all validation (parse, cycle check) before mutating anything; that gives atomic rejection for free.
Follow-up questions
- Add multiplication and parentheses. What changes in the parser and in evaluation?
- Instead of rejecting cycles, store the formula and make every variable on the cycle read as an error. How do you implement that and undo it later?
- Support `undo()` for the last define. What must you snapshot?
- How would this change if 100 users edit the same notebook concurrently?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.