Facts are stored; beliefs are derived. A live page holds a few facts — the symbol, the day, where the play-head was anchored — and shows many things computed from them: the bars for that symbol, the rows at the play-head, the trades of this session. In most frameworks the view fetches and computes those on every render, or an effect fetches them and stores a copy that goes stale. Crow has a form for them.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
use web, ui
qty = 100
price = 1.79
derive cost
return qty * price
fn more[]
qty = qty + 100
view render[]
return stack[p['`qty` shares cost `cost`'], button['More', click = more]]
test cost_follows_qty[]
a = app[render]
assert['cost 179' in html[a]]
click[a, more]
assert['cost 358' in html[a]]
page['/', render]derive cost is read like a fact and never assigned — cost = 5 in a
handler is an error that tells you to change qty or price instead. It
is recomputed only when something it read changed: the handler wrote
qty, so the next render recomputes cost; a render where nothing
changed serves the cached value.
A helper that only reads state is a derivation too. A fn band[] that
returns the config's price band is refused — a function sees its
parameters and nothing else — and derive band is what it was all along:
a value that follows the config, read by the handlers as band.
Both execution modes obey the same read sets: the facts and other
derivations a body names, transitively through the functions it calls
and inside interpolated strings; now[]; and the tables its queries
read. There is nothing to declare.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
use web, ui, db
conn = db.connect['sqlite://:memory:']
sym = 'IMRN'
fn setup[]
exec[conn, 'CREATE TABLE IF NOT EXISTS trades (id INTEGER PRIMARY KEY, symbol TEXT, px REAL)']
derive trades
return query[conn, 'SELECT id, symbol, px FROM trades WHERE symbol = ?', [sym]]
derive summary
return '`sym`: `len[trades]` trades'
fn buy[]
exec[conn, 'INSERT INTO trades (symbol, px) VALUES (?, ?)', [sym, 1.79]]
view render[]
return stack[p[summary], button['Buy', click = buy]]
test a_write_recomputes_the_query[]
a = app[render]
click[a, setup]
assert['IMRN: 0 trades' in html[a]]
click[a, buy]
assert['IMRN: 1 trades' in html[a]]
page['/', render]trades reads the table trades; the handler buy writes it, so the
derivation is recomputed on the next read and summary, which reads
trades, follows. A derivation whose value did not change — an equal
number, string or bool — cuts the chain: nothing downstream recomputes.
A table another system writes — a day's bars in ClickHouse — cannot be invalidated by anything in your program. A derivation over it says how old it may be:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
use web, ui, db
ch = db.connect['clickhouse://clickhouse.example:9000/market']
sym = 'IMRN'
day = '2026-09-04'
derive bars[fresh = 300]
return query[ch, 'SELECT ts, o, hi, lo, c, vol FROM bars WHERE symbol = ? AND date = ?', [sym, day]]
view render[]
return table[bars]
page['/', render]
serve[3000, refresh = 1]The default is fresh = 0: as fresh as the loop, recomputed every
refresh tick, which is exactly what a query inside a view did before.
Declaring fresh is the relaxation, never the other way round. A
derivation that reads now[] recomputes every tick too, which is how a
play-head derived from the clock stays honest.
A recompute never holds the page. A derivation that queries is computed
off the render: the tick renders at once with the value it has, and again
when the new value lands, and until its first value it reads as "loading"
(chapter 12.4 has the rules). In crow test every event settles before
the next line, so a test always reads the landed value.
Who holds a derivation is inferred from what it reads. A fact a session
writes belongs to it — assigned by a handler, bound to an input the
reader types into, or carried in the page's address: their own symbol,
their play-head, the card the URL names — so a derivation that reads one
is computed per session, and so is one over a data source
(fetch/poll), whose answers land in each session that asks. A
derivation over constants, tables and the clock alone is the same for
everyone, so it is computed once for the app and every session reads
it: the day's universe, the latest bar in a table, a scanner's config.
Database handles are not facts, so query[conn, …] over a table
nobody's handler touches is shared. A table write in any session reaches every session's
derivations over that table: when one reader buys, the blotter of
another reader watching the same book follows at once — the write wakes
every page that read the table, with no refresh tick and no code.
A derivation that reads only a slice of a table is recomputed only when a
write may have touched that slice. Every one of its queries of the table
has to fix one column by a fact, as WHERE board = ? with [board_id]
does. A local that is only the fact also counts, including the fact as
text (b = 'board_id'). A card added to board 2 then leaves every page showing board 1 as it
was. A write says which rows it touched when its WHERE is equalities
joined by AND, or an INSERT names its values. A write that does not
say (WHERE qty > ?, a card moved by its id alone, which does not say
its board) reaches every reader of the table, as before.
crow check prints the read set and the holder of each derivation, and a
slice by its column, so the split is never a guess:
derivations — what each reads, and who holds it:
bars: reads day, sym · tables bars (external, fresh 300s) — held by each session (it reads 'sym', which a session writes)
cards: reads board_id · tables cards where board = board_id — held by each session (it reads 'board_id', which a session writes)
universe: reads day · tables datasets — held by the app
A route has no session, so what its handler reads is what the app holds: a derivation over tables, the clock and facts no session moves. That is the cache a web service keeps — the rows it would otherwise ask the database for on every request:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
use web, db
book = db.connect['sqlite://notes.db']
made = exec[book, 'CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)']
derive notes
return query[book, 'SELECT id, body FROM notes ORDER BY id']
fn list_h[req]
return json[notes]
fn add_h[req]
conn = db.connect['sqlite://notes.db']
exec[conn, 'INSERT INTO notes (body) VALUES (?)', [req.body]]
return text['added']
route['/notes', list_h]
route['/add', 'POST', add_h]
serve[8080]The handler reads notes once, as its request begins: the app's value,
computed by the first request and kept until this program writes the
table. /add's write lands as its handler ends, so the next request
reads the new rows — no key to clear, no copy to refresh. Requests that
arrive together while the rows are computed wait for that one query. A
table something else writes is read through fresh: derive notes[fresh = 60] keeps its rows a minute, and at fresh = 0, as over
the clock, each request computes its own. A web program that declares a
derivation holds state as a page program does, so each of its top-level
names gets one value, which its derivations read; a route's handler
still sees none of them, and opens its own connection to write.
A route that reads a derivation a session holds is refused before the program runs, with what makes it the session's:
'band' is held by each session, and a route's handler has no session — it reads 'picked', which a session writes: a route reads a derivation the app holds, one over tables, the clock and facts no session moves
A list rendered from data is patched by position unless its rows say
who they are. Give a row's root an id built from its item's key and the
live client matches it to the same row in the previous render: a
deletion in the middle removes one node, a reorder moves nodes, and
nothing else on the page is rebuilt. Elements without an id still
match by position.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
use web, ui
items = [1, 2, 3]
view item_row[i]
return p['item `i`', id = 'r-`i`']
derive rows
out = for i in items
item_row[i]
return out
fn drop_middle[]
items = [1, 3]
view render[]
return stack[stack[rows], button['Drop', click = drop_middle]]
test rows_carry_their_identity[]
a = app[render]
assert['id="r-2"' in html[a]]
click[a, drop_middle]
assert['id="r-2"' not in html[a]]
assert['id="r-3"' in html[a]]
page['/', render]Because nodes survive a render, so does everything the browser keeps on them: the field a reader is typing in keeps its focus and caret while the rest of the page updates around it.
Before derive, the finch simulation's view ran four queries a second,
three of which changed a few times an hour. crow check warned about
it — remote-query-in-view — and could only warn. With derive, no view
queries anything: the bars are a derivation over the symbol and the day,
the rows at the play-head over the day and the clock, the trades over
the session. A tick recomputes the play-head and the rows; a symbol
change recomputes the bars; a buy recomputes the trades. Nothing else
moves, and nobody wrote a line about caching.
The checker now teaches the form instead of warning about its absence.
A top-level variable assigned from a query and read by a view — a
launch-time snapshot served forever — and a query against a remote
database inside a view are errors in crow check, each with a
one-key rewrite: d turns trades = query[…] into derive trades, and
hoists a rows = query[pg, …] line out of a view as
derive rows[fresh = 60] above it, the view reading rows as before.
Derivations are reactive state, so they live in a use ui program with
a page. In the test harness html[a] is a tick and click[a, h] marks
what the handler wrote, so a test can watch a derivation follow its
inputs the way these blocks do.