A procedure in Odin is a value, just like an integer. It has a type — written as its signature, proc(int, int) -> int — and a variable of that type holds the address of one compiled procedure. Below, op is one such variable: a single slot. Reassign the slot and the exact same call expression op(6, 2) runs a different procedure.
Level 1 was what it is. Now: the case where a proc value isn't a curiosity but the right tool — when the behavior to run is data, picked at runtime by a key. That's a dispatch table: an array (or map) whose element type is a proc type.
Map each key to a procedure once, in a literal. Then table[kind] looks up the proc value for that key and the (8, 3) after it calls whatever it found. The same two-character lookup-and-call serves every key — adding a new behavior is adding a row to the table, not editing the call site.
The counterfactual: if you only ever ran one fixed operation, you'd just write that operation — no table, no proc values, nothing. The table earns its keep precisely when the operation isn't known until the program is running: it arrives as an Op read from input, a config file, a packet, a script. The key is the data; the table turns the data into the procedure to run.
The other way to turn a key into a behavior is a branch ladder that re-lists every case at every call site:
The branch ladder costs you at every site. Every place that needs to dispatch re-writes the same switch. Add a fourth operation and you must hunt down and edit each of those sites — miss one and that site silently keeps doing the old set. The dispatch table collapses all of it to one table[kind](…): the cases live in one literal, and every site that indexes the table gets the new behavior for free. Same key-to-behavior idea, one copy instead of many, and the call site never grows.
Level 2 sold you on the slot. Level 3 is the bill — two edges that come with storing behavior in a variable: the slot can be empty, and the type is strict. Both are checkable; one is checked for you at compile time, the other you guard at runtime.
The bill, part one — the slot can be empty. The zero value of a proc type is nil: a declared-but-unassigned slot holds no procedure. Calling through it has no address to branch to, so the program crashes on the indirect call (it exits with a nonzero, failing status). The safe path guards the slot first:
This is why an optional callback — a proc value stored in a struct field, set only sometimes — is almost always invoked as if cb != nil { cb(…) }. The default state of a callback field is the empty slot; the guard is one compare-to-zero, the cheapest branch there is.
The bill, part two — the type is strict. A proc type is its whole signature. Two proc types match only if parameter count, parameter types, and return type all line up. A three-argument proc is a different type from a two-argument one, and the compiler refuses the assignment by name — at build time, before the program ever runs:
Binop :: proc(a, b: int) -> int div3 :: proc(a, b, c: int) -> int { return a / b / c } op: Binop = add op = div3 // a 3-arg proc into a 2-arg slot Error: Cannot assign value 'div3' of type 'proc(int, int, int) -> int' to 'Binop' Expected: int, int Got: int, int, int
The last level is the property that emerges once behavior lives in a slot: you can swap what a system does at runtime by writing one variable. The call sites don't change; the slot does.
Put a proc value in a struct field — a plug-in point. The rest of the program calls through that field every frame, never touching it. Change the mode and you reassign just the field; every call site downstream now runs the new behavior. Flip the mode below and watch the same step() call produce a different result:
The emergent payoff: the behavior became a value you can move. Reassigning one 8-byte field re-routes every world.step(a, b) in the program at once — no branch threaded through every call site, no re-compile, no class to subclass. The same move scales out: a per-state tick function for an actor's behavior, an on_click handler set when a widget is built, a comparator handed to a sort, a parser's per-token rule. Each is one slot you can read, fill, guard, and reassign — the whole vocabulary from the first three levels, applied.
That's the arc: L1 a proc is a value — one 8-byte slot holding a code address, reassign it and the same call runs a different proc → L2 a table of those slots turns a runtime key into the behavior to run → L3 the bill is an empty slot (guard != nil) and a strict signature (the compiler checks it) → L4 because behavior is now a movable value, one assignment re-routes the whole program.