Kinds
The atom roles Nu provides. Each kind is a point in the two-dimensional space of mutation and cardinality, plus the composition glue that ties mutating atoms into larger programs.
| Kind | Mutates Context | Yields | Cardinality | Role |
|---|---|---|---|---|
| Ref | no (own) | address / value | SCALAR | address atom; only path to Context |
| ScalarQuery | no | one value | SCALAR | pure value producer |
| StreamQuery | no | many values | STREAM | pure stream producer |
| Command | yes | nothing | VOID | pure mutator |
| ScalarAction | yes | one value | SCALAR | mutates and yields one |
| StreamAction | yes | many values | STREAM | mutates and yields many |
| Flow | no (own) | nothing | VOID | composes mutating children |
| Span | transparent | transparent | TRANSPARENT | cross-cutting wrapper |
Deep dives per kind come later. This doc is the high-level breakdown and the composition rules between kinds.
Ref
A name for a location. The only atom that touches Context. Two roles in one:
- a symbol naming an address;
- a Nu atom that, in a value slot, yields the value at that address.
Examples: user_ref, config["theme"], counter, orders[i].
Refs are the seam. Every READ, WRITE in a Nu program happens at a Ref. No Refs in a subtree, no mutation in the subtree.
Query
An atom that yields a value and never mutates Context on its own (its subtree may still mutate via Action descendants). Pure expression-level work: arithmetic, comparison, projection, iteration that produces values.
Splits on cardinality: a ScalarQuery yields one value, a StreamQuery yields many.
Examples: Add(a, b), Eq(x, y) (ScalarQuery); ItemsOf(coll_ref), Map(items, f) (StreamQuery).
Command
An atom that mutates Context and yields nothing. The pure mutator: it writes through one or more Ref children and that is its only output.
Examples: Store(ref, value), Copy(src, dst), Append(list_ref, item), Delete(ref).
Action
An atom that mutates Context and yields a value. The deliberate hybrid: real-world operations that atomically write and return (pop, swap, INSERT...RETURNING, POST /resource → id).
Like Query, Action splits on cardinality: a ScalarAction mutates and yields one value, a StreamAction mutates and yields many (Drain a queue, DELETE ... RETURNING over a predicate).
Examples: Pop(list_ref), Swap(ref, new_value), Create(collection_ref, payload) (ScalarAction); Drain(queue_ref) (StreamAction).
Action exists because modelling these as Command-then-Query would lose atomicity. One syscall, one transaction, one network roundtrip becomes two unrelated steps.
Flow
An atom that composes mutating children into a larger mutation-shaped program. Yields nothing. Mutates nothing on its own. Its body slot doesn't need a value; it needs a mutator (Command, Action, or another Flow whose own leaves are mutators).
Examples: Sequential(c1, c2, ...), IfDo(cond, body), ForEachDo(items, body), Parallel(c1, c2).
Flow's child should contain a mutating atom, otherwise it is meaningless (yields nothing, no mutation downstream -> no observability).
Span
A transparent wrapper for cross-cutting concerns: snapshotting, transactions, retry, fallback. It wraps any Nu and forwards the body's yield in the same shape and protocol. The Span atom is always a Span; parents slot-fit it by looking through to the body's yield, not by re-typing.
Examples: Snapshot(body), Transaction(body), Retry(body, policy), TryCatch(body, handler).
Span doesn't change the composition matrix below: it inherits its body's slot fit.
Composition matrix
Rows are parents, columns are children. Cells: ✓ accepted, ✗ rejected.
| parent | Ref | Query | Command | Action | Flow |
|---|---|---|---|---|---|
| Ref | ✓ | ✓ | ✗ | ✓ | ✗ |
| Query | ✓ | ✓ | ✗ | ✓ | ✗ |
| Command | ✓ | ✓ | ✗ | ✓ | ✗ |
| Action | ✓ | ✓ | ✗ | ✓ | ✗ |
| Flow | (p) | (p) | ✓ | ✓ | ✓ |
(p) = accepted only as a parameter slot (cond, items, count), never as a body peer.
One rule decides every cell
The cells are applications of mutation. Examples:
- A yielding parent (Q / A) must ride a mutating ancestor chain - so its slots feed that chain, and a child that doesn't yield has nothing to feed.
- A non-yielding parent (Flow) draws its observability from the subtree - so its body slot must hold a mutator.