Mica has no function pointers. A routine is called with its argument list, never read by name, and that is a design decision the whole language leans on: every call graph is static, every capture is visible, and nothing on a page can secretly run something the page does not show.
The C world’s libraries, meanwhile, hand out their events through exactly the
thing Mica refuses. sqlite3_exec wants a function pointer to fire once per
row. qsort wants one to compare. Sooner or later a real program meets one of
these, and the question is how to serve it without bending the law.
The answer is the third declared subprogram kind, beside task and
generator: the callback.
The kind
type
Tally = record
rows : int64;
end;
callback CountRow(context : pointer Tally, columns : int32, values : pointer int64, names : pointer int64) : int32;
begin
context.rows := context.rows + 1;
CountRow := 0;
end;A callback is a routine with a C-compatible shape — a return value or none, exactly as the C side declares it. What makes it a kind of its own is the law attached to the word:
- A foreign library performs every firing. Mica never calls a callback.
Writing
CountRow(...)anywhere is a compile error that says so. - Its name stands in exactly one place: as the argument at a callback slot — a parameter an imported function’s contract declares for it. As a value, an operand, or an ordinary argument, the name is refused.
- Its whole environment is explicit. A callback body reads its parameters and the unit’s own root storage — nothing else. The state it works on lives in the context record whose address your call passes through the library’s userdata parameter, and the library hands that pointer back to every firing, untouched. Reaching into an enclosing routine’s frame is refused with a sentence that names the fix: move the state into the context record.
That last rule is what keeps the capture visible. When you read
rc := SqliteExecRaw(db, "select reading from m", CountRow, address tally, address sink);everything the callback can touch is on that line: CountRow is the code,
address tally is the state. There is no hidden environment to wonder about,
which is the same WYSIWYG the value model promises everywhere else.
The slot
The contract side declares which parameter is the slot and what shape belongs in it — the class stating when the library fires the callback, the C signature a bound callback must match, and which sibling parameter carries the userdata. The contracts reference has the exact form.
The class matters. A during_call slot fires only while the imported call
itself runs — sqlite’s row callback, qsort’s comparison — so your frame
provably outlives every firing. A registered slot stores the callback and
fires it after the call returns, possibly from a thread the library owns; that
class is declared today and refused at every binding, until the road that
serves it arrives on the task model’s own terms.
You will rarely write one
The callback is the boundary tool, not the daily idiom. A curated library
wraps the callback-shaped C road in the shapes Mica already reads well — the
sqlite library’s daily road is Query, the binds, for row in Rows(q), the
column readers — and the callback stays for the places where the C shape is
the honest one.
The sqlite library is the kind’s first user: the pull
road for every day, the callback road on sqlite3_exec when you want it.