Temporal Zippers: Time-Traveling State for Systems Programming
One of the most unusual features in Nic is Temporal Zippers.
Instead of thinking about mutation as 'changing a variable in place', Nic models state as a timeline that you navigate through. You don't update a value – you move forward (or backward) in time.
This gives you:
- Pure, replayable state evolution
- Built-in time-travel debugging
- Cheap branching and backtracking
- A path to formal verification with temporal logic
- All while compiling down to zero-overhead SSA in the simple cases.
In this post, we'll walk through the core ideas behind Temporal Zippers, why they exist, and how they compile away.
From mutation to timelines
In most languages, you write:
let x = 0;
x = x + 1;
x = x * 2;In Nic, this kind of direct mutation is not allowed. Instead, you describe how 'x' changes over time inside a 'timeline' block:
fn main() -> i32 {
timeline x = 0 {
evolve x + 1; // time 1
evolve x * 2; // time 2
return x; // 2
}
}Conceptually:
time 0: x = 0
time 1: x = 1
time 2: x = 2 (current)Accessing the past: '@'
Because state is modeled as a timeline, reading the past becomes first-class:
fn demonstrate_history() -> i32 {
timeline x = 0 {
evolve x + 1;
evolve x * 2;
let initial = x@0;
let middle = x@1;
let current = x;
return initial + middle + current;
}
}Rewinding and branching
fn rewind_example() -> i32 {
timeline x = 0 {
evolve x + 1;
evolve x * 2;
rewind();
evolve x + 10;
return x;
}
}Under the hood, Nic uses ARC + copy-on-write for efficient branching.
Time-travel debugging built in
fn buggy_calculation(n: i32) -> i32 {
timeline (result = 0, i = 0) {
while i < n {
if i == 5 {
evolve (result * 0, i + 1);
} else {
evolve (result + i, i + 1);
}
}
debug {
for t in 0..timeline_length() {
println('At time {}: result = {}', t, result@t);
}
}
return result;
}
}Transactions: checkpoint & rollback
fn transfer_money(from: Account, to: Account, amount: f64)
-> Result[unit, string]
{
timeline (from_bal = from.balance, to_bal = to.balance) {
checkpoint();
evolve (from_bal - amount, to_bal);
if from_bal < 0 {
rollback();
return Err('Insufficient funds');
}
evolve (from_bal, to_bal + amount);
let total_before = from.balance + to.balance;
let total_after = from_bal + to_bal;
if total_before != total_after {
rollback();
return Err('Transaction inconsistency');
}
commit();
return Ok(());
}
}Comonads and context-aware computation
fn running_statistics(data: []f64) -> []Statistics {
timeline value = data[0] {
for x in data[1..] { evolve x; }
return extend(fn(t: Timeline[f64]) -> Statistics {
let history = t.past ++ [t.present];
let mean = sum(history) / len(history);
let variance =
sum(history.map(fn(x) -> (x - mean) * (x - mean))) / len(history);
return Statistics {
mean: mean,
variance: variance,
std_dev: sqrt(variance),
min: min(history),
max: max(history),
count: len(history)
};
});
}
}Zero-cost abstractions
Linear timelines like:
timeline x = 0 {
evolve x + 1;
evolve x * 2;
return x;
}Compile down to:
let x_0 = 0;
let x_1 = x_0 + 1;
let x_2 = x_1 * 2;
return x_2;Why Temporal Zippers?
They rethink what 'state' means in systems programming:
- state becomes time-structured
- debugging becomes replayable
- branching becomes cheap
- verification becomes possible
Comparison:
| Approach | Pros | Cons |
|---|---|---|
| Mutable variables | simple, direct | hard to reason |
| Pure FP | clean | needs GC |
| Rust ownership | safe | no shared history |
| Temporal Zippers | pure + efficient + debuggable | new mental model |
Want to go deeper?
See:
- Docs: https://learn.niclang.dev/guide/temporal-zippers
- Proposal: https://github.com/nicclang/nicc/blob/main/docs/proposals/0006-temporal-zippers.md
Next up:
Explicit Laziness: Strict by Default, Lazy When You Ask.