Ref
A name for a location. Plus a small extension that makes it behave as a Nu atom too.
A row in a table, a key in a dict, a file at a path, an API endpoint, an in-memory slot. The Ref is the symbol that points at it. The location lives in a fabric; the Ref lives in the Nu tree.
Dual role
A Ref is two things at once:
- A symbol - names an address. This is what a Ref fundamentally is.
- A Nu atom - when used as a child in another Nu, the Ref executes and yields the value at its address.
We deliberately stretch (1) into (2) for two reasons:
- Uniformity. Every Nu's children are Nus. Treating a Ref as just a symbol would make it a special non-Nu citizen. Folding execution into the Ref keeps the tree homogeneous (closure-like - every component is a Nu).
- DX. Without execution, every value-position use of a Ref would need a wrapper Query:
Add(Load(ref), 1). Tedious, noisy. With the dual role,Add(ref, 1)works directly - the Ref self-yields its value.
Because the Ref is now a Nu atom, it can also have children. Dynamic resolution: orders[currentref] - the dynamic index is a Query-like child (Query or Ref, since both yield values).
Cost: composition-time effects
The dual role costs a small amount of coherence in the effect system. Class-time annotation alone isn't enough.
A Ref bound into any (parent_atom, Ref child) slot pair acts as a value-yielding child - that is, in read role. The composition-time rule kicks in and attributes (ref, READ) to the parent's edge dynamically. This applies uniformly:
- Ref bound into an Interaction's Query slot - parent Interaction gets
(ref, READ). - Ref bound as a dynamic-key child of another Ref - the outer Ref gets
(ref, READ)on its own edge.
This is the only construction-time annotation in the model. We accept it as the price of the dual role.
Properties
- Own effects: none on a leaf Ref. A Ref with Ref children (dynamic resolution) picks up composition-time READ tuples on its own edge for each Ref child. No class-time effects ever - the kind declares nothing.
- Children: Query-shaped slots for dynamic key parts, evaluated as values. Queries go in directly; Refs go in via the dual role (binds in read role). A navigated Ref also holds its parent in a structural slot (
children[0]), resolved into an address rather than read - see Shapes. No Commands or Flows - producing an address must not transform Context. - Yields: the address, or the value at the address. The parent's slot decides. See slot roles below.
Slot roles
What the parent does through the Ref. Three roles:
| Role | What the parent does | Effect attributed to parent |
|---|---|---|
| read | reads the value at the address | (ref, READ) |
| write | writes a value to the address | (ref, WRITE) |
| structure | resolves address structure, never reads it | none |
A read slot is any value slot a Ref binds into (the dual role; composition-time). A write slot is a mutation slot - the kind declares it in mutates. A structural slot holds address structure - a hierarchical Ref's parent (children[0]; keys sit in children[1:]), a subscription's source - marked structural; it is resolved into an address, never value-read, so it carries no effect.
Determining the address is a prerequisite of both read and write, never a tracked effect on its own. A subscription's source (ReactDo, OnChange) is address structure, not a value: it sits in a structural slot, resolved to open the handle but never value-read, so it carries no effect. The ongoing nature of the subscription is a property of the kind.
Copy(src_ref, dst_ref)
# ^^^^^^^^ ^^^^^^^^
# read writesrc_refsits in Copy's Query slot 0. Dual role fires: composition-time(src_ref, READ)attributed to Copy.dst_refsits in Copy's mutation slot 1, declaredmutates = frozenset({1}).(dst_ref, WRITE)attributed to Copy.
Final tracked effects on Copy: {(src_ref, READ), (dst_ref, WRITE)}.
Forms
ref # static, leaf
ref[query] # dynamic key
ref[query1].field[query2] # nested dynamicA static Ref is a leaf - no children, no effects. A dynamic Ref has Query-shaped slots for its key parts; the same composition-time READ rule fires when a Ref binds in (Refs in the user's notation, e.g. orders[currentref]).
Examples
| Expression | Meaning |
|---|---|
user_ref | row in users table |
config["theme"] | key in config dict |
file("/tmp/log") | file at path |
api.users | endpoint |
counter | in-memory slot |
orders[refcache.current+1] | dynamic index, Q child |
Constraints
- Children must yield a value. Ref slots need a value to use (the address part, or the dynamic key). Commands and Flows yield nothing, so they're rejected. Actions yield, so they're accepted - the WRITE Action carries lands like any other tracked effect (e.g.
myref[Pop(list_ref)].fetch()). - The only atom that touches Context. Every interaction with Context - every READ, WRITE - happens at a Ref.
Why a separate kind
Refs isolate the Context seam. Anywhere a Nu touches the world, the touch goes through a Ref. That isolation is what makes the effect algebra work: read off the Ref children of an atom and you read off the atom's effects. No Refs in the subtree, no effects in the subtree.