Replay

In H.G. Wells' The Time Machine, ,} the traveler moves freely through time, witnessing the rise and fall of civilizations. Our event log grants us a similar power---not over physical time, but over the time of our system.

Given a complete history of events, we can reconstruct the state at any moment. We can travel backward and forward. We can watch the story unfold.

Deriving State from Events

The fundamental operation of replay is simple:

    replay(events, up_to_index):
        state = initial_state
        for each event in events[0..up_to_index]:
            state = apply(state, event)
        return state

Or more declaratively:

    state_at(n) = reduce(events[0..n], apply, initial_state)

To see move 15, we replay the first 15 events. To see the current state, we replay all events.

Time Travel

With replay, we gain extraordinary powers:

Go to any moment:

python
    state_at_move_10 = replay(events, up_to: 10)
    state_at_move_50 = replay(events, up_to: 50)
    state_at_start   = replay(events, up_to: 0)  // = initial_state

Undo is just "go to the previous moment":

python
    current_move = 23
    undo = replay(events, up_to: current_move - 1)

Redo is "go forward again":

haskell
    redo = replay(events, up_to: current_move)  // back to where we were

Two Roads to Time Travel

Let's compare how mutable and immutable approaches handle time travel:

Immutable (Event Sourcing):

python
    // State is derived from events
    events = [e1, e2, e3, e4, e5]
    current_state = replay(events)
    
    // Undo: just replay fewer events
    previous_state = replay(events[0..4])
    
    // Go to any point: replay to that point
    state_at_3 = replay(events[0..3])

Mutable (Command Pattern):

python
    // State is modified in place
    // Each operation must record how to reverse itself
    
    history = [
        {do: "set age to 30", undo: "set age to 25"},
        {do: "set name to Bob", undo: "set name to Alice"},
    ]
    
    // Undo: apply the inverse operation
    undo():
        last = history.pop()
        apply(last.undo)

The trade-offs are illuminating:

Immutable/ReplayMutable/Inverse
Undo complexityTrivial (replay less)Must track inverses
Random accessReplay from startOnly sequential undo
StorageAll eventsState + undo stack
ComputationRe-derive stateDirect mutation

The mutable approach can be optimized. For example, you could compact the history:

python
    // Original history
    history = [
        "age = 10",
        "age = 20",
        "age = 30"
    ]
    
    // Compacted: only the net change matters
    compacted = ["age: was 10, now 30"]
    
    // Undo 3 steps? Just restore to 10.

This trades computation (compacting) for memory (shorter history). Real systems often use hybrid approaches---event logs with periodic snapshots, or undo stacks with intelligent merging.

The Power of Determinism

Replay only works if our system is deterministic. Given the same events in the same order, we must always arrive at the same state.

python
    // Deterministic: always gives same result
    apply(state, event) = new_state      // pure function
    
    // Non-deterministic: might give different results!
    apply(state, event) = 
        new_state with timestamp: now()  // depends on external time

If apply consults external data (current time, random numbers, network calls), replay will produce different results each time. The time machine breaks.

Solutions:

  1. Store everything needed in the event itself (capture time when event is created)
  2. Use deterministic pseudo-random numbers with stored seeds
  3. Isolate non-determinism at system boundaries

Watching the Story Unfold

Replay isn't just for computation---it's for understanding.

python
    // Animate a chess game
    for move_number in 1..total_moves:
        state = replay(events, up_to: move_number)
        display(state)
        pause(1 second)

We can also analyze:

python
    // Find when the queen was captured
    for move_number in 1..total_moves:
        state = replay(events, up_to: move_number)
        if not has_queen(state, white):
            return move_number
haskell
    // Find all positions where black was in check
    check_positions = 
        [1..total_moves]
        |> filter((n) => is_in_check(replay(events, n), black))

The Cost of Replay

Replay is powerful but not free. To reach move 1000, we must process 1000 events.

python
    // Slow: replay from beginning every time
    state_at_1000 = replay(events, 1000)     // processes 1000 events
    state_at_1001 = replay(events, 1001)     // processes 1001 events!

If we access recent states often, this is wasteful. We keep re-doing the same work.

Caching the Journey

We can optimize by keeping intermediate states:

python
    // Cache recent state
    cached_state = replay(events, 1000)
    cached_index = 1000
    
    // To get state 1001: continue from cache
    state_at_1001 = apply(cached_state, events[1001])

Or cache at regular intervals:

sql
    snapshots:
        at_move_0:    initial_state
        at_move_100:  replay(events, 100)
        at_move_200:  replay(events, 200)
        ...
    
    // To reach move 237: start from snapshot 200, replay 37 events
    state_at_237 = replay(events[201..237], starting_from: snapshots[200])

This hybrid approach---events plus periodic snapshots---gives us the best of both worlds: full history and fast access.

Debugging with Replay

When something goes wrong, replay lets us investigate:

    // Bug report: "Game crashed at move 47"
    
    // Replay to just before the crash
    state_before = replay(events, 46)
    
    // Examine the state
    inspect(state_before)
    
    // Try the problematic event
    problematic_event = events[47]
    
    // Step through slowly, watching what happens
    debug_apply(state_before, problematic_event)

We can reproduce bugs exactly. We can step through the history. We can find the moment things went wrong.

We've seen two approaches: storing events (and replaying to get state) versus storing state directly. Each has trade-offs. In the next chapter, we compare them systematically: snapshots versus stories.