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?

  1. Undo: Return to the previous state
  2. Redo: Return to the state before undo (if we change our mind)
  3. Multiple levels: Undo many steps, not just one
  4. Persistence: Survive program restart

The Immutable Solution

With our event-sourced architecture, undo is almost trivial:

haskell
    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 + 1

That'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?

python
    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: 4

This 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:

python
    switch_to_branch(1)    // see D → E timeline
    switch_to_branch(2)    // see F timeline

Implementation Sketch

yaml
    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 + 1

The 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:

yaml
    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.