loom

package module
v0.0.0-...-9caed14 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 12, 2026 License: AGPL-3.0-or-later Imports: 11 Imported by: 0

README

Loom

Loom is a Go library for building reactive functional state machines with immutable state snapshots. You declare refs, actions, derived values, and watches, then dispatch actions through a runtime that applies pure state transitions and reactive cascades.

Install

go get github.com/andrewbreksa/loom
import "github.com/andrewbreksa/loom"

Core Ideas

Loom follows a three-step lifecycle:

  1. Assembly: declare refs, actions, watches, derived values, and patterns with loom.New().
  2. Build: create a runtime with Build().
  3. Dispatch: execute transitions with Dispatch(name, args).

State is stored as an immutable flat map with dot-separated keys (for example, player.alice.health). Every change is represented as []Rebind, which keeps transition logic pure and replayable.

Quickstart (Runnable Example)

package main

import (
	"fmt"

	"github.com/andrewbreksa/loom"
)

func main() {
	rt := loom.New().
		Ref("wallet.alice", 100).
		Ref("wallet.bob", 0).
		Action("transfer", loom.NewAction(
			func(s loom.StateView, args map[string]any) bool {
				from := loom.String(args["from"])
				amount := loom.Int(args["amount"])
				return loom.Int(s.Get(from)) >= amount
			},
			func(s loom.StateView, args map[string]any) []loom.Rebind {
				from := loom.String(args["from"])
				to := loom.String(args["to"])
				amount := loom.Int(args["amount"])
				return []loom.Rebind{
					s.Rebind(from, loom.Int(s.Get(from))-amount),
					s.Rebind(to, loom.Int(s.Get(to))+amount),
				}
			},
		)).
		Build()

	err := rt.Dispatch("transfer", map[string]any{
		"from": "wallet.alice",
		"to":   "wallet.bob",
		"amount": 30,
	})
	if err != nil {
		panic(err)
	}

	fmt.Println(rt.Get("wallet.alice")) // 70
	fmt.Println(rt.Get("wallet.bob"))   // 30
}

The Seven Primitives

Primitive Purpose
Ref Declare initial key/value state.
Derived Pure value recomputed after state changes.
Watch Reactive callback that runs when matching keys change.
Action Guarded state transition (Permits + Effect).
Pattern Named reusable effect ([]Rebind factory).
For Namespace iteration via ForEach(ns, fn).
Apply Convert descriptions into []Rebind via StateView.Apply.

Common Patterns

Grid or Matrix Initialization

Use Spread to generate many refs from a template:

decls := loom.Spread(
	"cells.{R}.{C}.owner",
	"none",
	loom.IntRange("R", 0, 3),
	loom.IntRange("C", 0, 3),
)
Turn Order and Linked Sequences

Use Chain to encode next-player relationships:

decls := loom.Chain("turn.after", []string{"alice", "bob", "carol"}, true)
Symmetric Mappings

Use Pair for bidirectional lookups:

decls := loom.Pair("opposite", [][2]string{{"north", "south"}})

Runtime Behavior

Dispatch(action, args) runs this sequence:

  1. Resolve action by name.
  2. Execute Permits(state, args) guard.
  3. Execute Effect(state, args) to produce []Rebind.
  4. Apply rebinds to create a new immutable state snapshot.
  5. Recompute all Derived values.
  6. Fire matching Watch handlers (including cascades).
  7. Append event and history entries.

If Permits returns false, Dispatch returns PermitError.

OpenTelemetry Integration

Loom can emit traces and metrics using OpenTelemetry. Integration is optional; if you do nothing, Loom uses the global no-op providers.

import (
	"context"

	"github.com/andrewbreksa/loom"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

tp := sdktrace.NewTracerProvider()
defer tp.Shutdown(context.Background())

rt := loom.New().
	WithTelemetry(loom.TelemetryOptions{
		TracerProvider:      tp,
		InstrumentationName: "my-service/loom",
	}).
	Ref("x", 0).
	Action("inc", loom.NewAction(
		func(_ loom.StateView, _ map[string]any) bool { return true },
		func(s loom.StateView, _ map[string]any) []loom.Rebind {
			return []loom.Rebind{s.Rebind("x", loom.Int(s.Get("x"))+1)}
		},
	)).
	Build()

_ = rt.DispatchContext(context.Background(), "inc", nil)

Instrumented dispatches include:

  • Span name: loom.dispatch
  • Span attributes: loom.action, loom.args_count, loom.result, loom.rebind_count, loom.watch_callback_count
  • Metrics: loom.dispatch.total, loom.dispatch.duration.seconds, loom.dispatch.rebinds, loom.dispatch.watch_callbacks

Event Sourcing and Replay

Runtimes keep history and an event log:

events := rt.EventLog()
history := rt.History()

rt2 := loom.New().
	Ref("x", 0).
	Action("inc", loom.NewAction(
		func(_ loom.StateView, _ map[string]any) bool { return true },
		func(s loom.StateView, _ map[string]any) []loom.Rebind {
			return []loom.Rebind{s.Rebind("x", loom.Int(s.Get("x"))+1)}
		},
	)).
	Build()

_ = history
if err := rt2.Replay(events); err != nil {
	panic(err)
}

Testing and Validation

go build ./...
go test ./...
go test -run TestTicTacToe ./...
go test -run TestChess ./...

API Reference (At a Glance)

  • Assembly: New, Ref, Refs, Derived, Watch, Action, Pattern, Module, WithTelemetry, Build
  • Runtime: Dispatch, DispatchContext, Get, GetOr, Namespace, Length, Snapshot, History, EventLog, Replay
  • Helpers: NewAction, R, ForEach, Spread, Chain, Pair
  • Telemetry: TelemetryOptions
  • Type coercions: String, Int, Float, Bool, Slice

License

This project is licensed under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later). See LICENSE for details.

Documentation

Overview

Package loom implements a reactive functional programming framework built on seven primitives: ref, derived, watch, action, pattern, for, apply.

The entire runtime is one function:

func step(env Env, action Action) Env {
    if !action.Permits(env) { return env }
    rebinds := action.Effect(env)
    newEnv  := env.Apply(rebinds)
    watches := fireWatches(env, newEnv)
    return fold(newEnv, watches)
}

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Bool

func Bool(v any) bool

Bool coerces a ref value to bool.

func Float

func Float(v any) float64

Float coerces a ref value to float64.

func Int

func Int(v any) int

Int coerces a ref value to int.

func Slice

func Slice(v any) []any

Slice coerces a ref value to []any.

func String

func String(v any) string

String coerces a ref value to string.

Types

type Action

type Action interface {
	Permits(state StateView, args map[string]any) bool
	Effect(state StateView, args map[string]any) []Rebind
}

Action is a permitted state transition. Permits() is a pure guard. Effect() returns descriptions of changes, never executes them.

func NewAction

func NewAction(
	permits func(state StateView, args map[string]any) bool,
	effect func(state StateView, args map[string]any) []Rebind,
) Action

type ActionFunc

type ActionFunc struct {
	// contains filtered or unexported fields
}

ActionFunc is a convenience Action from two functions.

func (ActionFunc) Effect

func (a ActionFunc) Effect(state StateView, args map[string]any) []Rebind

func (ActionFunc) Permits

func (a ActionFunc) Permits(state StateView, args map[string]any) bool

type ApplyFn

type ApplyFn func(description any) []Rebind

ApplyFn is the logic-layer boundary.

type DerivedDecl

type DerivedDecl struct {
	Key  string
	Name string
	Fn   DerivedFn
}

DerivedDecl binds a key to a DerivedFn.

type DerivedFn

type DerivedFn func(state StateView) any

DerivedFn is a pure function over the env.

type Env

type Env struct {
	// contains filtered or unexported fields
}

Env is an immutable snapshot of the ref graph. Every Rebind produces a new Env. History is a slice of Envs — free, because immutable.

func NewEnv

func NewEnv() Env

func (Env) Apply

func (e Env) Apply(rebinds []Rebind) Env

Apply returns a new Env with all rebinds applied.

func (Env) Diff

func (e Env) Diff(other Env) []Rebind

Diff returns the keys that changed between e and other.

func (Env) Get

func (e Env) Get(key string) any

Get returns the value for a key, or nil if not set.

func (Env) GetOr

func (e Env) GetOr(key string, def any) any

GetOr returns the value for a key, or the default if not set.

func (Env) Has

func (e Env) Has(key string) bool

Has returns true if the key exists.

func (Env) Length

func (e Env) Length(prefix string) int

Length returns the count of keys under a namespace prefix.

func (Env) Namespace

func (e Env) Namespace(prefix string) map[string]any

Namespace returns all keys that start with prefix.

func (Env) Set

func (e Env) Set(key string, value any) Env

Set returns a new Env with the key rebound to value.

func (Env) Snapshot

func (e Env) Snapshot() Env

Snapshot returns a copy of the current env.

func (Env) String

func (e Env) String() string

String returns a JSON representation.

func (Env) ToMap

func (e Env) ToMap() map[string]any

ToMap returns the underlying data as a plain map.

type Event

type Event struct {
	Seq    int
	Action string
	Signal string
	Args   map[string]any
	Before Env
	After  Env
}

Event is a record in the event log. For action events, Action is set and Signal is empty. For signal events, Signal is set and Action is empty.

type ForFn

type ForFn func(key string, value any) []Rebind

ForFn iterates over a namespace, producing rebinds.

type InvariantDecl

type InvariantDecl struct {
	Name string
	Fn   InvariantFn
}

InvariantDecl binds a name to an InvariantFn.

type InvariantError

type InvariantError struct {
	Violations []InvariantViolation
}

InvariantError is returned when one or more invariants are violated after a dispatch or signal emission settles.

func (InvariantError) Error

func (e InvariantError) Error() string

type InvariantFn

type InvariantFn func(state StateView) []error

InvariantFn is a global rule evaluated on settled state after each dispatch or signal emission. Return nil or an empty slice for success; return errors to signal violations.

type InvariantViolation

type InvariantViolation struct {
	Invariant string
	Err       error
}

InvariantViolation holds a single invariant failure.

type Loom

type Loom struct {
	// contains filtered or unexported fields
}

Loom assembles declarations into a Runtime.

l := loom.New()
l.Ref("game.state", "active")
l.Action("attack", AttackAction{})
l.Watch("player.*.life", onLifeChange)
rt := l.Build()
rt.Dispatch("attack", map[string]any{"target": "bob", "amount": 5})

func New

func New() *Loom

New creates a new Loom assembler.

func (*Loom) Action

func (l *Loom) Action(name string, action Action) *Loom

func (*Loom) Build

func (l *Loom) Build() *Runtime

Build returns a Runtime initialized with all registered declarations.

func (*Loom) Derived

func (l *Loom) Derived(key string, fn DerivedFn) *Loom

func (*Loom) Invariant

func (l *Loom) Invariant(name string, fn InvariantFn) *Loom

Invariant registers a global rule evaluated on settled state after each dispatch or signal emission. Dispatch fails if any invariant returns errors.

func (*Loom) Module

func (l *Loom) Module(m Module) *Loom

Module loads all declarations from a Module.

func (*Loom) OnSignal

func (l *Loom) OnSignal(signal string, fn OnSignalFn) *Loom

OnSignal registers a handler for the named signal. Handlers may return rebinds that cascade through the normal watch/derived chain.

func (*Loom) Pattern

func (l *Loom) Pattern(name string, fn PatternFn) *Loom

func (*Loom) Ref

func (l *Loom) Ref(key string, value any) *Loom

func (*Loom) Refs

func (l *Loom) Refs(decls ...RefDecl) *Loom

Refs adds multiple ref declarations at once.

func (*Loom) Selector

func (l *Loom) Selector(name string, pattern string) *Loom

Selector registers a named, reusable scope over refs identified by pattern. The pattern uses the same dot-separated glob syntax as Watch patterns.

func (*Loom) Watch

func (l *Loom) Watch(pattern string, fn WatchFn) *Loom

func (*Loom) WithTelemetry

func (l *Loom) WithTelemetry(options TelemetryOptions) *Loom

WithTelemetry configures OpenTelemetry integration for runtimes built from this Loom instance.

type Module

type Module interface {
	Register() *Registry
}

Module is the unit of organization in Loom. A game, a service, or a domain is a Module. Modules return a Registry of their declarations.

type OnSignalFn

type OnSignalFn func(state StateView, sig Signal) []Rebind

OnSignalFn handles an emitted signal and may return rebinds.

type PatternFn

type PatternFn func(state StateView, args ...any) []Rebind

PatternFn is a named, reusable effect.

type PermitError

type PermitError struct {
	Action string
}

PermitError is returned when an action's Permits() returns false.

func (PermitError) Error

func (e PermitError) Error() string

type Rebind

type Rebind struct {
	Key   string
	Value any
}

Rebind is a description of a state change. Actions return []Rebind — they never mutate directly.

func ForEach

func ForEach(ns map[string]any, fn ForFn) []Rebind

ForEach runs fn over every key in namespace, returning all rebinds.

func R

func R(key string, value any) Rebind

type RefDecl

type RefDecl struct {
	Key   string
	Value any
}

RefDecl is a ref declaration at init time.

func Chain

func Chain(namespace string, sequence []string, ring bool) []RefDecl

Chain generates linked ref pairs from a sequence.

Chain("turn.after", []string{"alice", "bob", "carol"}, true)
→ turn.after.alice=bob, turn.after.bob=carol, turn.after.carol=alice

func Pair

func Pair(namespace string, pairs [][2]string) []RefDecl

Pair generates symmetric ref pairs.

Pair("opposite", [][2]string{{"north", "south"}, {"east", "west"}})
→ opposite.north=south, opposite.south=north, ...

func Spread

func Spread(pattern string, value any, ranges ...SpreadRange) []RefDecl

Spread generates a flat list of (key, value) pairs from a pattern and ranges.

Spread("pit.{N}.stones", 4, IntRange("N", 1, 7))
→ [("pit.1.stones", 4), ("pit.2.stones", 4), ...]

type Registry

type Registry struct {
	Refs       []RefDecl
	Derived    []DerivedDecl
	Watches    []WatchDecl
	Actions    map[string]Action
	Patterns   map[string]PatternFn
	Invariants []InvariantDecl
	Signals    []SignalDecl
	Selectors  map[string]SelectorDecl
}

Registry holds all declarations before the runtime is built. Modules register into a Registry. Loom assembles them into a Runtime.

func NewRegistry

func NewRegistry() *Registry

func (*Registry) AddAction

func (r *Registry) AddAction(name string, action Action)

func (*Registry) AddDerived

func (r *Registry) AddDerived(key string, name string, fn DerivedFn)

func (*Registry) AddInvariant

func (r *Registry) AddInvariant(name string, fn InvariantFn)

func (*Registry) AddPattern

func (r *Registry) AddPattern(name string, fn PatternFn)

func (*Registry) AddRef

func (r *Registry) AddRef(key string, value any)

func (*Registry) AddSelector

func (r *Registry) AddSelector(name string, pattern string)

func (*Registry) AddSignal

func (r *Registry) AddSignal(signal string, name string, fn OnSignalFn)

func (*Registry) AddWatch

func (r *Registry) AddWatch(pattern string, name string, fn WatchFn)

func (*Registry) Merge

func (r *Registry) Merge(other *Registry)

type Runtime

type Runtime struct {
	// contains filtered or unexported fields
}

Runtime is the fold.

func (*Runtime) Dispatch

func (rt *Runtime) Dispatch(name string, args map[string]any) error

Dispatch runs an action by name.

func (*Runtime) DispatchContext

func (rt *Runtime) DispatchContext(ctx context.Context, name string, args map[string]any) (err error)

DispatchContext runs an action by name with a caller-provided context for telemetry propagation.

func (*Runtime) Emit

func (rt *Runtime) Emit(name string, args map[string]any) error

Emit fires a named signal into the runtime. Signal handlers may return rebinds; these cascade through the normal watch/derived/invariant chain. The signal is captured in the event log.

func (*Runtime) EmitContext

func (rt *Runtime) EmitContext(ctx context.Context, name string, args map[string]any) error

EmitContext is Emit with a caller-provided context for telemetry propagation.

func (*Runtime) EventLog

func (rt *Runtime) EventLog() []Event

func (*Runtime) Get

func (rt *Runtime) Get(key string) any

func (*Runtime) GetOr

func (rt *Runtime) GetOr(key string, def any) any

func (*Runtime) History

func (rt *Runtime) History() []Env

func (*Runtime) Length

func (rt *Runtime) Length(prefix string) int

func (*Runtime) Namespace

func (rt *Runtime) Namespace(prefix string) map[string]any

func (*Runtime) Replay

func (rt *Runtime) Replay(events []Event) error

func (*Runtime) Select

func (rt *Runtime) Select(name string) map[string]any

Select returns all env keys matching the named selector.

func (*Runtime) Snapshot

func (rt *Runtime) Snapshot() Env

type SelectorDecl

type SelectorDecl struct {
	Name    string
	Pattern string
}

SelectorDecl is a named, reusable scope over refs. The Pattern uses the same dot-separated glob syntax as Watch patterns.

type Signal

type Signal struct {
	Name string
	Args map[string]any
}

Signal is a first-class occurrence emitted into the runtime. Signals model facts that happened even when no ref changes materially.

type SignalDecl

type SignalDecl struct {
	Signal string
	Name   string
	Fn     OnSignalFn
}

SignalDecl registers a handler for a named signal.

type SpreadRange

type SpreadRange struct {
	Name   string
	Values []any
}

SpreadRange is one axis of a spread.

func IntRange

func IntRange(name string, start, end int) SpreadRange

IntRange creates a SpreadRange for integers [start, end).

func Range

func Range(name string, values ...any) SpreadRange

Range creates a SpreadRange from a slice.

type StateView

type StateView interface {
	Get(key string) any
	GetOr(key string, def any) any
	Has(key string) bool
	Namespace(prefix string) map[string]any
	Length(prefix string) int
	Pattern(name string, args ...any) []Rebind
	Rebind(key string, value any) Rebind
	Apply(description any) []Rebind
	// Select returns all env keys matching the named selector.
	Select(name string) map[string]any
}

StateView is the read-only view of the env exposed to Action, Watch, Derived, Invariant, and OnSignal functions.

type TelemetryOptions

type TelemetryOptions struct {
	TracerProvider      trace.TracerProvider
	MeterProvider       metric.MeterProvider
	InstrumentationName string
}

TelemetryOptions configures OpenTelemetry integration for Loom runtimes.

If providers are nil, Loom uses the global providers from the otel package.

type WatchDecl

type WatchDecl struct {
	Pattern string
	Name    string
	Fn      WatchFn
}

WatchDecl binds a pattern to a WatchFn.

type WatchFn

type WatchFn func(state StateView, key string, value any) []Rebind

WatchFn fires when a watched ref changes.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL