Functional Updates and State Diffing TODO
Concept
A functional update leaves the existing data structure untouched and instead returns a new version that reflects only the change. The key technique that makes this cheap is path copying: only the nodes along the path from the root to the modification point are copied, while the rest of the subtrees are shared by pointer with the previous version (structural sharing). In balanced trees or trie-based structures like HAMTs, the path length is O(log n), so a single update is bounded by O(log n) node copies. But the real cost hides not in the asymptotic complexity but in the constant factor. Every update allocates new nodes, which increases allocation and GC pressure, and scatters nodes across the heap, which hurts cache locality due to more pointer chasing. On the upside, since the previous version stays intact, diffing two versions can skip entire subtrees whose references are identical and only walk the parts that actually changed.
In frontend and state-machine code that relies on immutable state, it's common to assume "copying is O(log n), so it's basically free" — and then watch throughput collapse under GC spikes and cache misses. On the flip side, the fact that diffing can be done by reference comparison is exactly what justifies re-render and change-propagation optimizations.
Code & Formula
# Day 4: 함수형 업데이트와 상태 diff — copy-on-write의 실제 비용
# 불변 이진트리(배열)를 path copying으로 갱신하고, 두 버전의 diff를 노드 공유 여부로 빠르게 계산한다.
class Leaf:
__slots__ = ("value",)
def __init__(self, value):
self.value = value
class Branch:
__slots__ = ("left", "right")
def __init__(self, left, right):
self.left = left
self.right = right
def build(values):
if len(values) == 1:
return Leaf(values[0])
mid = len(values) // 2
return Branch(build(values[:mid]), build(values[mid:]))
def update(node, index, value, size):
if size == 1:
return Leaf(value)
half = size // 2
if index < half:
return Branch(update(node.left, index, value, half), node.right) # right는 그대로 공유
return Branch(node.left, update(node.right, index - half, value, size - half))
def collect(node, out):
if isinstance(node, Leaf):
out.append(node.value)
else:
collect(node.left, out)
collect(node.right, out)
def count_leaves(node):
return 1 if isinstance(node, Leaf) else count_leaves(node.left) + count_leaves(node.right)
def diff(a, b, offset, changed):
if a is b:
return # 포인터가 같으면 서브트리 전체가 동일 -> 즉시 종료
if isinstance(a, Leaf):
if a.value != b.value:
changed.append(offset)
return
mid = offset + count_leaves(a.left)
diff(a.left, b.left, offset, changed)
diff(a.right, b.right, mid, changed)
n = 8
v0 = build(list(range(n)))
v1 = update(v0, 5, 999, n)
v2 = update(v1, 2, -1, n)
out0, out1, out2 = [], [], []
collect(v0, out0); collect(v1, out1); collect(v2, out2)
print("v0 =", out0)
print("v1 (index5 갱신) =", out1)
print("v2 (v1에서 index2 갱신) =", out2)
changed01 = []
diff(v0, v1, 0, changed01)
print("v0 -> v1 diff =", changed01, ", left 서브트리 공유:", v0.left is v1.left)
changed12 = []
diff(v1, v2, 0, changed12)
print("v1 -> v2 diff =", changed12, ", right 서브트리 공유:", v1.right is v2.right)
docs/code/algorithms/algorithms-4.py
Exercise
Implement a hash map three ways — (1) full copy on every update, (2) HAMT-style path copying, and (3) a mutable map — and measure execution time, allocation volume, and GC time over 100,000 insertions to compare them.
Practical Connection
The EVM stacks state changes in a journal for snapshot/rollback on revert, and the state trie itself produces a new root every block via path copying — a direct on-chain instance of a copy-on-write structure. Verex's in-memory order book runs into the same trade-off when designing snapshot-based rollback or per-version diff transmission.
Where it lands in Jayverse
- Verex: benchmark before choosing path copying for the order book. Turn the trade-off the concept describes into an actual decision — measure GC pressure and cache-miss cost of HAMT-style snapshots against a mutable book under real order volume, then pick, instead of assuming O(log n) makes it free.
- Wallet: give transaction simulation the same structural sharing. When a user edits a simulated transaction before signing, snapshot and revert speculative state cheaply with path copying rather than deep-copying the whole simulated state each edit.
- gitboard: diff dashboard states by reference, not by value. If gitboard shows how a service's state changed between refreshes, skip subtrees whose references are unchanged instead of deep-diffing the whole payload each time.
Key expressions
| Expression | 뜻 · 쓰이는 자리 |
|---|---|
| hide in | ~에 숨어 있다 · 진짜 비용이 점근 복잡도가 아니라 상수항에 숨어있음을 설명 · "the real cost hides not in the asymptotic complexity" |
| scatter across | ~에 흩어지다, 산재하다 · 새 노드들이 힙 여기저기에 흩어져 캐시 효율을 해침 · "scatters nodes across the heap" |
| pointer chasing | 포인터를 따라가며 메모리 접근하는 것 · 흩어진 노드 때문에 포인터 추적이 잦아짐 · "hurts cache locality due to more pointer chasing" |
| on the upside | 반면 좋은 점은, 긍정적인 면으로는 · 구조 공유 덕분에 diff가 빨라진다는 대목 도입 · "On the upside, since the previous version stays intact" |
| skip (entire subtrees) | (전체 부분트리를) 건너뛰다 · 참조가 같은 부분은 비교할 필요 없이 넘어감 · "diffing two versions can skip entire subtrees" |
| basically free | 사실상 공짜다, 거의 비용이 없다 · 로그 복잡도라 비용이 없다고 착각하는 흔한 가정 · "copying is O(log n), so it's basically free" |
| bounded by | ~로 상한이 정해지다, ~이내로 제한되다 · 업데이트 1회가 O(log n) 복사로 제한됨 · "a single update is bounded by O(log n) node copies" |
| HAMT | 해시 배열 매핑 트라이(Hash Array Mapped Trie, HAMT) · 경로 복사로 구조적 공유를 구현하는 불변 자료구조. "trie-based structures like HAMTs, the path length is O(log n)" |
| GC | 가비지 컬렉션(Garbage Collection, GC) · 매번 새 노드를 할당하는 함수형 업데이트가 늘리는 부담. "increases allocation and GC pressure" |
| copy-on-write | 카피온라이트(복사 후 쓰기, copy-on-write) · 변경분만 복사하고 나머지는 공유하는 구조, EVM 상태 트리도 이 방식. "a direct on-chain instance of a copy-on-write structure" |
If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.