nustack

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:

  1. A symbol - names an address. This is what a Ref fundamentally is.
  2. 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:

RoleWhat the parent doesEffect attributed to parent
readreads the value at the address(ref, READ)
writewrites a value to the address(ref, WRITE)
structureresolves address structure, never reads itnone

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      write
  • src_ref sits in Copy's Query slot 0. Dual role fires: composition-time (src_ref, READ) attributed to Copy.
  • dst_ref sits in Copy's mutation slot 1, declared mutates = 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 dynamic

A 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

ExpressionMeaning
user_refrow in users table
config["theme"]key in config dict
file("/tmp/log")file at path
api.usersendpoint
counterin-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.

On this page