cell - shared state
A thread-safe mutable box. The one place in Ecko where sharing mutable state is intentional.
hits = cell(0)
http.serve(8080, fn(req) {
n = cell_update(hits, fn(v) v + 1) # atomic read-modify-write
http.text("request #{n}")
})
Why it exists
Parallel workers - pmap, http.serve handlers, async tasks - each see a snapshot of captured variables. Mutating an ordinary mut inside one changes that worker's copy and nothing else. That makes races impossible by default, and it makes a shared counter impossible too.
A cell is the escape hatch, and it is deliberately a different thing with a different name, so that shared mutable state is visible in the source rather than implied by where a variable was declared.
The operations
| call | effect |
|---|---|
cell(initial) | Create a cell |
cell_get(c) | Read the current value (a copy) |
cell_set(c, v) | Replace the value |
cell_update(c, fn) | Run fn(current) under the lock, store the result, return it |
Use cell_update for anything read-modify-write
cell_update(hits, fn(v) v + 1) # atomic
cell_set(hits, cell_get(hits) + 1) # races - two workers can read the same v
The second form is two operations with a gap between them. cell_update holds the lock across the read and the write, which is the only way a counter is correct under concurrency.
Cells share on assignment
a = cell(0)
b = a # b and a are the same cell
cell_set(b, 5)
cell_get(a) # 5
Everything else in Ecko has value semantics - binding a list to a new name gives you an independent list. A cell shares, and that is exactly why it survives being captured into a spawned task.
The one trap
Do not touch the same cell inside its own cell_update. The lock is not reentrant, so it deadlocks:
cell_update(c, fn(v) cell_get(c) + 1) # deadlock
cell_update(c, fn(v) v + 1) # v is already the current value
The value you need is the argument. Reaching for the cell again inside the update is always avoidable.
Cells and structure
A cell can hold any value, including a map or a list, so a small amount of shared application state fits in one:
state = cell({ users: [], connections: 0 })
cell_update(state, fn(s) insert(s, "connections", s.connections + 1))
For anything that must outlive the process, use std.sql or the filesystem. A cell is process memory.