lazily (Go)
Lazy reactive primitives for Go — the Cell kernel (Source / Computed /
Effect, all cells guarded; eager via Computed.Eager()) with automatic
dependency tracking and cache invalidation, plus the full lazily-spec
wire protocol, CRDT collection types, keyed cell collections, Harel state
charts, and the distributed CRDT plane.
A Go port of the lazily reactive family (lazily-rs, lazily-py,
lazily-kt, lazily-js, lazily-dart, lazily-zig)
— conformant with lazily-spec and lazily-formal. The
concurrency surfaces (async reactive context, signaling room, CRDT anti-entropy
plane) are built on goroutines and channels: share state by communicating,
not by locking.
go get github.com/lazily-hub/lazily-go
The reactive family — the Cell kernel
Two value kinds, named without collision (design #lzcellkernel). Cell is a
conceptual word for a value-bearing reactive node, not a Go type — the two kinds
are two concrete handle structs, and write protection lives in the type:
Source[T]— a value written from outside the graph; the only kind withSet/Merge. It folds writes under aMergePolicy(KeepLatest by default = a plain cell; Sum/Max = the formerMergeCell).Cell ≡ Source<KeepLatest>. Constructors:NewSource/NewSourceWithPolicy.Computed[T]— a value computed from upstream; lazily cached and dependency-tracking, with neitherSetnorMerge.NewComputed(f)is guarded (an==-guard suppresses equal recomputes) — the sole derived constructor now that the formerMemois removed: aComputedis the guarded form.computed.Eager()makes it eager (which replaces the formerSignal), returning the same handle;computed.Lazy()reverses it.Effect— a side-effect sink (ctx.effect), outside the Cell hierarchy.
The T comparable bound is what the guard needs. For a value type that is not
comparable, drop to NewSlot(f) — the bound-free storage-sense primitive
(T any, no guard), the escape hatch mirroring lazily-rs's slot().
The v1 Cell[T] read-genus interface is dropped: no Go generic code used it
as a bound, and v2 no longer needs a genus for write protection.
Write protection lives in the types: because Computed has no Set,
computed.Set(…) does not compile. Values are lazy by default; call Eager
on a Computed for eager push-style semantics. Use Effect for side effects (a
sink, outside the Cell hierarchy).
Usage
import lazily "github.com/lazily-hub/lazily-go"
ctx := lazily.NewContext()
a := lazily.NewSource(ctx, 2)
b := lazily.NewSource(ctx, 3)
// Lazy: computes on first read, caches, recomputes only when a or b changes.
sum := lazily.NewComputed(ctx, func(c *lazily.Context) int { return a.Get() + b.Get() })
sum.Get() // 5
a.Set(10)
sum.Get() // 13
// Eager: Eager() attaches a puller so the computed re-materializes on every change.
parity := lazily.NewComputed(ctx, func(c *lazily.Context) string {
if a.Get()%2 == 0 {
return "even"
}
return "odd"
}).Eager()
parity.Get() // "even"
a.Set(11)
parity.Get() // "odd" (already updated before the read)
To react to a Cell from outside the graph (the hook for UI bridges), declare a
dependency edge with an Effect — a Cell has no callback registry:
count := lazily.NewSource(ctx, 0)
effect := lazily.NewEffect(ctx, func(*lazily.Context) func() {
fmt.Println("now", count.Get()) // the Get is what subscribes
return nil
})
count.Set(1) // prints "now 1"
effect.Dispose()
An Effect runs once at creation and then once per settled cascade, so under
Batch it observes the settled value rather than each intermediate write. When
you need every individual transition delivered as a stream, use TopicCell.
Batch coalesces cascades so dependent Effects flush once:
ctx.Batch(func() {
a.Set(1)
b.Set(2)
}) // a single coalesced cascade
Competing-consumer work queue
WorkQueueCell[T] provides exclusive FIFO claims, visibility deadlines,
worker-scoped acknowledgements, tail retries, and bounded dead-letter handling.
Item ids remain stable across retries; every claim gets a fresh delivery id.
The same contract is available as ThreadSafeWorkQueueCell for competing
goroutines and AsyncWorkQueueCell for composition on an AsyncContext; queue
and topic equivalents follow the same ThreadSafe* / Async* naming.
work := lazily.NewWorkQueueCell[string](ctx, 10, 3)
work.Push("job")
delivery, _ := work.Claim("worker-a", 100)
if !work.Ack("worker-a", delivery.DeliveryID) {
panic("ack rejected")
}
Context
All reactives that react to each other must share a Context. It holds an
identity-keyed cache and the computation stack used for automatic dependency
tracking. Context is single-goroutine; for concurrent access use the
lock-backed ThreadSafeContext or drive the graph from one owner goroutine via
the channel-serialized AsyncContext.
Reactive members on a struct
Go has no decorators, so there is no direct analog of lazily-py's @slot /
@cell on a method. The idiomatic Go equivalent of a lazily-decorated method
is to wire the reactive members as Source / Computed fields in the
constructor and expose thin accessor methods. The accessor reads like a plain
method but is lazy, cached, and dependency-tracked:
type Greeter struct {
Name *lazily.Source[string]
greeting *lazily.Computed[string] // the "decorated" lazy member
}
func NewGreeter(ctx *lazily.Context) *Greeter {
g := &Greeter{Name: lazily.NewSource(ctx, "")}
// greeting tracks Name automatically; it recomputes only after Name changes.
g.greeting = lazily.NewComputed(ctx, func(*lazily.Context) string {
return "Hello, " + g.Name.Get() + "!"
})
return g
}
// Greeting reads like a normal method but is lazy + cached + reactive.
func (g *Greeter) Greeting() string { return g.greeting.Get() }
ctx := lazily.NewContext()
g := NewGreeter(ctx)
g.Name.Set("World")
g.Greeting() // "Hello, World!" (computed on first read, then cached)
g.Name.Set("Go")
g.Greeting() // "Hello, Go!" (recomputed once, because Name changed)
NewComputed is guarded by default (it suppresses the downstream cascade when the
recomputed value is unchanged); chain .Eager() for eager recomputation. A
runnable version of this pattern lives in
example_test.go.
State machine
StateMachine is a finite state machine backed by a Source, so any
Computed reading its state is invalidated on transition.
State chart
StateChart is a full Harel/SCXML hierarchical state machine — the native
counterpart of lazily-formal's LazilyFormal.StateChart. It is
compute, not protocol (never serialized as a distinct wire kind). Built from
the declarative JSON form via ChartDefFromJSON. Implements compound states,
orthogonal (parallel) regions, shallow and deep history, entry/exit/transition
actions, and named fail-closed guards.
Collections & CRDTs
Keyed cell collections (SourceMap, SourceTree) with LIS move-minimized
reconciliation, the memoized semantic tree (SemTree), stable-id alignment, the
reactive queue (QueueCell — a FIFO collection whose shell invalidates by
reader kind: a push invalidates Len/IsEmpty (and Head when transitioning
from empty), a pop invalidates Head/Len/IsEmpty, and a bounded queue's
IsFull is the reactive backpressure signal; SPSC primitive with MPSC via
Batch, over a pluggable QueueStorage backend with the default
VecDequeStorage), and the CRDT family: free-text character CRDT (TextCrdt,
with delta sync), move-aware sequence CRDT (SeqCrdt), the lossless tree
CRDT (LosslessTreeCrdt — a single rooted concrete-syntax tree whose leaves
own every rendered byte, with op-based delta sync over a dotted non-contiguous
version frontier), registers (MvRegister, PnCounter, CellCrdt), and the
distributed CRDT plane (CrdtPlane, CrdtPlaneRuntime) with anti-entropy and
WebRTC transport + signaling.
Keyed reactive maps
ReactiveMap[K, V, H] is the unified keyed reactive collection (#reactivemap):
keys map to independently-tracked per-entry reactive nodes over a handle kind H
(*Source[V] input cells or *Computed[V] derived computeds), with reactive membership
and order. Go generics can't add methods to a type alias, so its two
specializations are thin distinct structs embedding *ReactiveMap with the
handle kind fixed:
SourceMap[K, V]— input-cell entries. Adds the source-onlySetand eager value-minting (Entry/EntryWith). Every entry is a writable*Source[V].ComputedMap[K, V]— derived-slot entries.GetOrInsertWithmints a slot on first access (lazy materialization);MaterializeAllpre-mints the keyset (eager). A slot's value is derived, soComputedMaphas noSet. There is no eager/lazy mode flag — eager is a pre-mint loop, lazy is mint-on-access.
These were named CellMap / SlotMap before the v2 kernel renamed the node
kinds to Source and Computed. The old spellings remain as deprecated
generic type aliases (CellMap = SourceMap, SlotMap = ComputedMap, plus
the Async* / ThreadSafe* flavors, and CellTree = SourceTree for the
ordered keyed tree) with deprecated constructor wrappers, so existing callers
keep compiling. Generic type aliases require Go 1.24+.
The shared surface — GetOrInsertWith / Remove / Move* / Keys / Len /
ContainsKey / membership + order signals — lives on the generic ReactiveMap.
ctx := lazily.NewContext()
// Lazy derived-slot map over a large keyed space; only read keys are allocated.
sheet := lazily.NewComputedMap[Key, int](ctx)
sheet.GetOrInsertWith(k, func(k Key) int { return recompute(k) }) // mint on first pull
sheet.PresentCount() // grows only with reads
Eager (MaterializeAll pre-mint) and lazy (GetOrInsertWith mint-on-access)
return identical values for every key (observational transparency); the
strategy changes allocation timing and memory, never results. The laws —
observe_canonical, eager_lazy_observationally_equivalent,
materialize_present_monotone / lazy_present_subset_eager, and entry-kind
orthogonality (cell_entries_materialized_in_every_mode /
slot_entries_deferred_under_lazy) — are proven in lazily-formal's
Materialization module and pinned by the conformance/materialization/*.json
fixtures. The Send + Sync (ThreadSafeSourceMap / ThreadSafeComputedMap) and async
(AsyncSourceMap / AsyncComputedMap) flavors mirror the same surface.
lazily-spec IPC
The IPC types (Snapshot, Delta, CrdtSync, NodeState, ...) implement the
language-agnostic lazily-spec wire protocol so a Go graph's state can
be mirrored to remote observers across processes and languages. They round-trip
the canonical fixtures from lazily-spec/conformance/. The C-ABI FFI
boundary (cgo) exposes the state plane to in-process native embedders.
The additive command / RPC message plane (command-plane-v1) —
CommandSubmit / CommandCancel / CommandEvents / CommandProjection plus
the CommandRpcClient facade — rides the same wire envelope. Terminal command
authority folds through a CausalReceipt, so a unary call resolves only on a
terminal receipt (never on a transport ACK or accepted/queued event).
Cross-process zero-copy transport
Large cell/slot payloads cross the IPC plane as descriptors, not copies
(#lzzcpy). The producer spills an oversized payload to a pluggable
BlobBackend and ships a small ShmBlobRef descriptor; the
receiver resolves the descriptor against the same backend and reads the
bytes in place — no copy, no checksum recompute. Three backends ship:
InProcessBackendwraps aShmBlobArena— the single-address-space case (the cgo FFI host / an in-process embedder).ArrowBackendholds Apache Arrow IPC stream bytes — the descriptor's bytes are an Arrow IPC stream a columnar consumer imports zero-copy.ShmBackend(Linux) is a genuine POSIXshm_open+mmapregion with an atomic bump allocator — the cross-process backend: a descriptor minted by one mapping resolves zero-copy against an independent mapping of the same region.
SpillMessage replaces oversized Inline/Payload sites across a
Snapshot/Delta/CrdtSync with descriptors above a deployment threshold; a
receiver-side BlobRouter resolves any descriptor by its backend
discriminator (a shm descriptor never resolves in an Arrow backend, and vice
versa). The backend field is optional and defaults to shm, so legacy
descriptors validate unchanged — the transport is a strict superset of the
shared-memory blob path. The backend-agnostic invariants (spill-then-resolve
identity, backend isolation, ABA generation safety, checksum integrity) are
proven in lazily-formal's ZeroCopyTransport.lean and pinned by the
delta_zero_copy_arrow conformance fixture.
Conformance
lazily-go replays the shared lazily-spec conformance fixtures (IPC,
keyed collections, Harel state charts, the lossless-tree CRDT, and the
command-plane message family) — asserting identical behavior to every other
binding. Run make check (fmt + vet + build + test) locally; CI also runs the
race detector.
Benchmarks
See BENCHMARKS.md for micro-benchmark results on the hot paths
— reactive core read/write, slot/memo recompute, batch coalescing, keyed
collections, and CRDT construction — with ns/op / B/op / allocs/op and
what each case measures. The reactive steady state (Cell read/write, SourceMap
insert/read) is zero-allocation. Benchmarks are defined as Go testing.B cases
in bench_test.go (mirroring the in-library RunBenchmarkSuite)
and reproducible with make bench (go test -bench=. -benchmem ./...).
BENCHMARKS.md also includes a spreadsheet-scale benchmark (make bench-scale)
on a graph of N input cells + N formula slots (=A_i + A_{i-1}): ~2M nodes
at the default N=1M, up to a full 10M-cell Google Sheets workbook at
LAZILY_SCALE_N=5000000. It builds the full workbook in under a second and — via
the lazy pull-based model — a one-cell edit + bounded-viewport read recomputes
only the viewport (~2 formulas), staying orders of magnitude cheaper than a full
recalc regardless of sheet size.
Feature coverage
Coverage by feature family across every binding, generated from
coverage.json in lazily-spec. Legend: ✅ shipped ·
~ partial · — absent · ⊘ not applicable. The canonical matrix with per-cell
notes and platform carve-outs lives in
lazily-spec § Cross-Language Coverage.
Summary — family × language
| Family | Rust | Python | Kotlin | JS | Dart | Zig | Go | C++ | C# |
|---|---|---|---|---|---|---|---|---|---|
| Reactive graph | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Materialization | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Family sync | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Statecharts | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Keyed collections | ✅ | ✅ | ✅ | ✅ | ✅ | ~ | ✅ | ✅ | ✅ |
| Reactive queue | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Broadcast topic | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Work queue | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| CRDT data types | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Lossless tree | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Egress | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Ingress | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Wire codec | ✅ | ✅ | ✅ | ✅ | ~ | ✅ | ✅ | ✅ | ✅ |
| Transport & FFI | ✅ | ✅ | ✅ | ~ | ~ | ✅ | ✅ | ~ | ✅ |
| Message passing | ✅ | ✅ | ✅ | ✅ | ✅ | ~ | ✅ | ✅ | ✅ |
| Reliable sync | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ | ~ |
| Distributed plane | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Causal receipts | ~ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Security boundary | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Membership | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Coordination | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Presence | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Temporal | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Rate shaping | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Windowing | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Resilience | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Portable stdlib | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Service plane | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Instrumentation | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Roll-up rule: a family cell is ✅ only when every required row in that family is ✅; ~ when the family is mixed (some shipped or partial); — when no required row is shipped or partial; ⊘ only when every required row in the family is not applicable. Rows the spec marks MAY (optional) are excluded from the roll-up — declining an optional feature is not a gap.
A family cell summarises 70 feature rows. For row-level marks, per-cell notes, and platform carve-outs see the canonical coverage matrix in lazily-spec.
The lazily family
lazily is one reactive model implemented across many languages — the same cell kernel, the same keyed collections and CRDTs, and the same wire protocol — so peers written in different languages talk to each other without a translation layer.
lazily-spec— the language-agnostic wire protocol, the cross-language feature matrix, and the conformance corpus every binding replays, lazily-go included.lazily-formal— the Lean 4 formal model every binding inherits its proofs from.
| Repo | Language |
|---|---|
lazily-rs | Rust — the reference implementation |
lazily-py | Python |
lazily-go | Go — you are here |
lazily-kt | Kotlin / JVM |
lazily-js | JavaScript / TypeScript |
lazily-cs | C# / .NET |
lazily-cpp | C++ |
lazily-zig | Zig |
lazily-dart | Dart / Flutter |
lazily-react | React / Preact bindings layered over lazily-js (not a separate language binding) |