Odin · procedures & multiple returns

A procedure, four levels deep

A procedure is a value you declare with ::, exactly like a constant: name :: proc(params) -> returns { body }. The signature is the whole contract — what goes in, what comes out — and Odin lets the "what comes out" be more than one value at once. Hover the parts of the signature, then watch a call hand its values back into named slots.

The five parts of a declaration

The return clause is the part that grows. One value needs no parentheses; two or more are a parenthesized, named list — and the call site takes them apart in one binding. Pick a call and step the values out:

Outputs below are real stdout from compiled Odin programs (claims/lessons/03-procedures-and-multiple-returns).
the proc finishes — its return values come back in order
▾   positional binding: slot 1 → first name, slot 2 → second name, …

    

The grammar

square :: proc(x: int) -> int — one in, one out, no parens on the return.

minmax :: proc(a, b, c: int) -> (lo, hi, span: int) — three in, three out. Adjacent params (and returns) of one type share it.

What you're seeing

Level 1 was the shape. Level 2 is why the second value earns its place: the everyday job of returning a value and a fact about that value in one call. The signature carries both, so the caller can't use the answer without also seeing whether the answer is real.

Take a lookup, score_at(i), over scores := [4]int{40, 0, 55, 90}. It returns (value: int, ok: bool). The trap a single return walks into: index 1 legitimately holds 0, and a missing index also wants to report... 0. With one return value those two cases are indistinguishable. The ok bool is the whole point — it tells a real zero apart from an absent one:

score_at(1) — a real hit

value = 0 · ok = true
index 1 -> 0 (a real score)
the 0 is genuine — ok said so

score_at(9) — out of range

value = 0 · ok = false
index 9 -> missing (value was 0, ok was false)
same 0 — but ok flags it as absent

both lines above are real program output · the two cases print the same value and are told apart only by ok

The idiom this unlocks: the (value, ok) shape lets you fold the answer and its validity into one line: if v, ok := score_at(1); ok { … } — bind both, then branch on ok in the same breath. The value and the verdict travel together and can never drift apart.

The counterfactual: if every index were always valid — a fixed table you fully control — you wouldn't need the second value at all; a plain -> int would do. You reach for the extra return precisely when the result can be conditional, and the caller must not be allowed to forget the condition.

Why the alternatives are worse

a sentinel value return -1 for "not found" and a real number otherwise. breaks the moment -1 is a legal answer — and nothing forces the caller to check for it.
an out-parameter take a ^bool and write the found-flag through it. two ways for one call to report itself; the caller must pre-declare the bool and pass its address. More wiring, same idea.
a one-field struct wrap both into a Result struct and return that. a named type and a .value/.ok reach for every use — paperwork the multi-return makes vanish.

Each alternative either can't represent "absent" safely (the sentinel), splits one answer across two channels (the out-parameter), or adds a type and a field-access tax (the struct). The multi-return says the same thing with no sentinel to collide, no address to thread, and no wrapper to unwrap.

Level 2 sold you on the second value. Level 3 is the bill — and it's paid by the compiler, on your behalf: the return arity is a hard contract, checked on both sides. You cannot quietly drop a value, and you cannot quietly come up one short. Either slip stops the build.

A two-value proc demands a two-name landing on the caller side. Bind just one name with no _, and the count mismatch is a compile error — pick a violation:


    

The release valve: the contract isn't "use everything", it's "account for everything". When you genuinely don't want a value, you spend a single underscore on it — only_q, _ := divmod(20, 6) — and the build is happy. That one call prints:

// only_q, _ := divmod(20, 6)
20 / 6 -> 3 (remainder discarded)

Why this is a feature, not a nuisance: "0 means error" or a forgotten second value is how a real bug hides in plain sight. Odin moves that whole class of mistake to build time — the cost is one underscore when you mean to discard, and in exchange a dropped or missing return can never silently ship.

account for ≠ consume The check is on the count, not on use. only_q, _ := divmod(20, 6) still computes the remainder — the proc runs in full — you've just declined to name it. The _ is a real binding target that throws its value away; it is the one sanctioned way to drop a return. Leaving the name off entirely is the error you saw above.

The last level is the property that emerges from naming the returns: the names in the signature aren't decoration — they are real, zero-initialized locals in the body. That single fact makes the proc self-documenting, simplifies its control flow, and lets a bare return ship a well-defined answer from anywhere.

Name a return and you get a variable of that name, already set to its zero value, live for the whole body. You assign the ones you care about and let the rest stand at zero; a bare return sends the current values out. Watch the early-exit path — b == 0 assigns nothing and bare-returns:


      

    

The emergent payoff: the signature safe_div :: proc(a, b: int) -> (q: int, ok: bool) already tells you what the two outputs mean — q is the quotient, ok whether it's valid — before you read a line of the body. The names are the documentation, and because they're real zero-initialized locals, every exit path has a defined value with no extra wiring: an early return on the b == 0 path ships 0, false for free. One declaration carries the call shape, the validity signal, the docs, and the zero-default — all checked by the compiler.

That's the arc: L1 a proc is a value, and its return clause can hand back several values that unpack by position → L2 that lets one call return a value and a fact about it — the (value, ok) idiom — with no sentinel, out-param, or wrapper → L3 the arity is a contract the compiler enforces on both sides; _ is the sanctioned discard → L4 naming the returns makes them zero-initialized locals, so the signature self-documents and a bare return always ships a defined answer.

probes reproduce with odin run … -file · every output & the L3 errors are real compiler output (claims/lessons/03-procedures-and-multiple-returns)