The Undo Machine
Every artist knows the value of the eraser. Every writer knows the power of "undo." The ability to reverse a mistake---to return to a previous state---is one of the most humane features a system can offer.
We've built all the pieces. Now let's assemble them into something practical: a complete undo/redo system.
The Requirements
What do we want from an undo system?
- Undo: Return to the previous state
- Redo: Return to the state before undo (if we change our mind)
- Multiple levels: Undo many steps, not just one
- Persistence: Survive program restart
The Immutable Solution
With our event-sourced architecture, undo is almost trivial:
UndoSystem:
events: [event_1, event_2, ..., event_n]
current_position: n // where we are in history
current_state:
replay(events, up_to: current_position)
undo:
current_position = current_position - 1
redo:
current_position = current_position + 1That's it. No complex inverse operations. No careful state management. Just move a pointer.
The Branching Problem
What happens when we undo, then make a new move?
events: [A, B, C, D, E]
position: 5 // at event E
undo twice:
position: 3 // at event C
make new move F:
events: [A, B, C, F] // D and E are gone!
position: 4This is fine for many applications. But what if we want to keep everything?
The Tree Model
Instead of discarding, we can branch:
Original: A → B → C → D → E
After undo to C, then F:
A → B → C → D → E (branch 1)
\
F (branch 2, now active)Now we can navigate both branches:
switch_to_branch(1) // see D → E timeline
switch_to_branch(2) // see F timelineImplementation Sketch
TreeUndoSystem:
events: [all events ever, tagged with branch]
branches:
main: [A, B, C, D, E]
alt_1: [A, B, C, F]
current_branch: alt_1
current_position: 4
undo:
current_position = current_position - 1
redo:
current_position = current_position + 1
new_action(event):
if current_position < length(current_branch):
// We're in the past! Create new branch
new_branch = current_branch[0..current_position] + [event]
branches.add(new_branch)
current_branch = new_branch
else:
current_branch.append(event)
current_position = current_position + 1The User Experience
Tree undo is powerful but can be confusing. Most applications hide the complexity:
- Simple mode: Linear undo, branches discarded silently
- Advanced mode: Show branch points, let users navigate
- Hybrid: Linear by default, but branches preserved and accessible
Persistence: Surviving Restart
With events stored, persistence is straightforward:
save:
write(events, to: "game_history.json")
write(current_position, to: "game_state.json")
load:
events = read("game_history.json")
current_position = read("game_state.json")
current_state = replay(events, up_to: current_position)The game's save file is just its event log. Load the events, replay to the saved position, and you're back exactly where you were---with full undo history intact.
Try It Yourself
Try It Yourself
12 exercises to practice
We've seen how immutable state, events, and replay combine into a powerful undo system. But we've been working with chess the whole time. What patterns transcend chess? In the next part, we explore forms that recur across different domains.