Forking Paths

In Borges' "The Garden of Forking Paths," time is not a line but a labyrinth. At each moment, all possible futures branch outward. Every choice creates a new universe.

Our systems can embody this vision. From any state, we can explore multiple futures. We can ask "what if?" and follow each answer to its conclusion.

The What-If Question

At move 15 of our chess game, White is considering two moves:

    current position at move 15
    
    option A: queen d1 → h5    // aggressive attack
    option B: knight f3 → e5   // solid development

Which is better? To answer, we must explore both futures.

Branching the Timeline

With immutable state, this is natural. We don't modify---we derive:

    state_15 = replay(events, up_to: 15)
    
    // Branch A: explore the aggressive line
    state_15a = state_15 with { queen d1 → h5 }
    state_16a = state_15a with { opponent responds... }
    state_17a = state_16a with { we continue... }
    ...
    
    // Branch B: explore the solid line
    state_15b = state_15 with { knight f3 → e5 }
    state_16b = state_15b with { opponent responds... }
    state_17b = state_16b with { we continue... }
    ...

The original state_15 still exists. Both futures branch from it. We can explore both, compare them, and choose.

The Tree of Possibilities

                    state_15
                    /      \
                   A        B
                  /          \
            state_15a      state_15b
             /    \          /    \
            ...   ...      ...    ...

Each node is a state. Each edge is a move (an event). The tree contains all possible futures from our starting point.

With immutable state, this tree is trivial to build. Each branch is just a new derivation:

    branch(state, event):
        state with event applied       // creates new node

Why Mutability Makes This Hard

In a mutable system, exploring futures is dangerous:

    // Mutable: exploring destroys the original
    
    current_state = ...at move 15...
    
    // Try option A
    apply_move(current_state, queen d1 → h5)  // mutates!
    
    // Oops! We can't try option B anymore
    // The original state_15 is gone

To branch in a mutable system, we must explicitly copy:

    // Mutable with explicit copying
    
    state_for_A = deep_copy(current_state)    // expensive!
    state_for_B = deep_copy(current_state)    // expensive!
    
    apply_move(state_for_A, queen d1 → h5)
    apply_move(state_for_B, knight f3 → e5)

Deep copying is expensive. Immutability gives us branching for free.

Practical Forking: Version Control

The most familiar forking system is Git:

    main branch:     A → B → C → D
                              \
    feature branch:            E → F → G

Git stores events (commits). Each commit is immutable. Branches are just pointers to commits. Creating a branch is instant---no copying required.

python
    git branch feature     // instant: just creates a pointer
    git checkout feature
    // make changes...
    git commit             // new event, appended to feature's history

The main branch is unchanged. The feature branch explores an alternative future. Both coexist.

Parallel Universes

Sometimes we want to explore many alternatives:

elixir
    // Chess engine: explore all legal moves
    all_futures = 
        get_legal_moves(current_state)
        |> map((move) => current_state with { move applied })
    
    // Evaluate each future
    evaluations =
        all_futures
        |> map((state) => evaluate(state))
    
    // Choose the best
    best_move = 
        zip(moves, evaluations)
        |> max_by((move, score) => score)

With immutability, these explorations are independent. They can run in parallel. No locks, no conflicts, no shared mutable state.

Keeping Track of Branches

When we have many branches, we need to organize them:

yaml
    branches:
        main:     [event_1, event_2, ..., event_n]
        analysis: [event_1, event_2, ..., event_n, alt_1, alt_2]
        whatif:   [event_1, event_2, ..., event_15, hypo_1, hypo_2]

The events before the fork are shared. Only the divergent events are unique to each branch.

    shared history:     [event_1, ..., event_15]
    
    main continues:     [event_16_main, event_17_main, ...]
    whatif explores:    [event_16_alt, event_17_alt, ...]

This is memory-efficient. The branches only store their differences from the common ancestor.

The Multiverse of Possibility

Our system now holds multiple timelines:

Each timeline is equally valid within the system. The distinction between "real" and "hypothetical" is external---a matter of which timeline we choose to actualize.

  • The main timeline (what actually happened)
  • Alternative timelines (what could have happened)
  • Hypothetical timelines (what we're exploring)

Paths can fork. But can they rejoin? What happens when parallel timelines must merge? In the next chapter, we confront the challenge of reconciling divergent histories.