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 developmentWhich 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 nodeWhy 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 goneTo 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 → GGit stores events (commits). Each commit is immutable. Branches are just pointers to commits. Creating a branch is instant---no copying required.
git branch feature // instant: just creates a pointer
git checkout feature
// make changes...
git commit // new event, appended to feature's historyThe main branch is unchanged. The feature branch explores an alternative future. Both coexist.
Parallel Universes
Sometimes we want to explore many alternatives:
// 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:
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.