Context protocol
How a Ref finds its Fabric inside a Context at evaluation time. The protocol has three dimensions: type, tags, predicates. Every binding and every lookup uses some subset.
Why a protocol, not a dict
A Context that was just name -> value would fix the binding to a name. Real programs need more:
- one composition runs against several kinds of resources - the Storage binding for
Orderdiffers from the one forUser; - one binding shards by site or path - which shard answers depends on a value computed at lookup time;
- a child Context extends a parent without polluting siblings - bindings narrow scope, they do not leak back up.
Context resolves these structurally. A lookup tells you not just what but which one under these conditions.
Three dimensions
Type
Primary key. Every binding and every lookup starts with a type - the kind of resource being asked for (Storage, View, Snapshot handle, an API client). The type decides which Fabric family is touched.
Tags
Scope refinements. Additional keys attached to a binding, with set semantics. More tags = more specific.
bind(Storage, rocksdb)
bind(Storage, order_db, tag=Order)
get(Storage) -> rocksdb
get(Storage, Order) -> order_dbSubset fallback. A lookup with (Storage, Order, Region) first tries the exact tag set, then every smaller subset ((Storage, Order), (Storage, Region), (Storage,)). The most specific match wins; the unscoped binding is the default.
Tags are opaque values. Any hashable identity - a shape class, a region marker, a lifecycle marker - can serve.
Predicates
Named guards on bindings. A predicate is a function attached to a binding; at lookup, the caller supplies data and the predicate decides whether the binding applies.
bind(View, shard_a, tag=Market, sharding=|site, path| site[0] < 16)
bind(View, shard_b, tag=Market, sharding=|site, path| site[0] >= 16)
get(View, Market, site=(5,)) -> shard_a
get(View, Market, site=(20,)) -> shard_bPredicates encode conditional resolution at the binding site. Multiple predicates on one entry are ANDed; multiple entries at the same (type, tags) scope are ORed.
Strictness. When guarded entries exist at a (type, tags) scope and the caller supplies data, predicates decide. If none match, the lookup falls back to smaller tag subsets. A guarded miss does not silently fall through to an unguarded entry at the same scope.
Resolution order
get(type, *tags, **data)
1. exact (type, tags):
if guarded entries exist and data was supplied:
return first whose predicates all match
if none match: skip this scope
else:
return direct entry if present
2. subset fallback: try every smaller subset of tags
(largest first, down to size 1); apply step 1 at each
3. empty-scope fallback: try (type, ∅); apply step 1
4. failSpecificity wins. Predicates gate. Fallback is structural.
Eager and lazy
Two binding modes, same protocol:
- eager - the value is held.
- lazy - a factory registered at bind time; runs once on first lookup and caches.
Lazy bindings defer construction until evaluation reaches that branch. Useful when the resource is expensive or has side effects.
Narrowing via Spans
Because Context is immutable, a Span narrows it for its body without touching siblings: internally the Span extends the Context with its own binding, hands the extended Context to the body, and the outside world keeps its unnarrowed view. The bracket scope and the binding scope are the same scope.
What the protocol does not do
- no global state. Every binding is on a Context value.
- no name resolution. The registry is typed, not string-keyed.
- no effect tracking. The Nu tree records what an Interaction does; Context is where the effect lands, not a log of what was done.
What it buys
- same tree, different Context, different behavior. Swap backends, shards, snapshots without rewriting the composition.
- conditional resolution at the binding site. Routing logic lives in
bind, not inifbranches across the program. - specificity without inheritance. Tag subsets give per-shape, per-region overrides without a class hierarchy of Contexts.
- scoped narrowing for free. A Span binds into a new Context; the bracket scope is the binding scope.