Odin · search-driven learning

How to learn Odin without guessing

There is no syntax in this lesson — it's a habit. When you don't know a proc's name or shape, you have one reliable loop: guess? → search the source of truth → read the real signature → call it → verify. The source of truth is the core: standard-library code itself, the overview, and the substrate's own index. Click each rung to walk it.

The move that anchors the loop: the moment you catch yourself about to invent a proc name — "surely there's a slice.minmax?" — that uncertainty is the trigger. You don't type the guess and hope it compiles, and you don't ask another person yet. You open the source and read, because the answer is sitting in a file you already have on disk: the substrate mirrors core: under ~/odin/dist/core/, and most of core:slice is more readable than any summary of it.

Why this beats asking first: the round trip to a file open in your editor is milliseconds; the round trip to a person or a chat box is seconds-to-minutes, and it can hand you a plausible-looking proc name that does not exist. The source can't lie about its own contents. And you finish the lookup with a citable path — the exact file and line — instead of a paraphrase you have to take on faith.

Level 1 was the loop. Level 2 runs it on a real question and shows the payoff: a proc you'd never have guessed the shape of, read straight from the source, then called. The question — "I need both the smallest and largest value of a slice in one pass. Is there a built-in?"

Walking the loop lands you in ~/odin/dist/core/slice/slice.odin, where reading down the file turns up min, then max, then — the one you wanted — min_max. Reading is the whole point here: you'd never have guessed it returns three values, or that the third one matters. Here is the exact signature, copied from the source:

core:slice — the real signature
min_max :: proc "contextless" (s: $S/[]$T) -> (min, max: T, ok: bool) where intrinsics.type_is_ordered(T)
min
the smallest element, in one pass
max
the largest element, same pass
ok
false when the slice is empty — the reason it's a third return, not an assumption

    

What reading the signature taught you that a name never would: the where intrinsics.type_is_ordered(T) clause says this works for any ordered element — int, f32, runes — not just numbers you had in mind. And the ok: bool return is the source telling you, in code, that an empty slice has no min or max, so it hands back a flag instead of a garbage answer. Both facts are in the signature; neither is in the name.

The guess you'd have written instead

the hand-rolled version — more code, more places to slip
lo, hi := scores[0], scores[0]   // assumes scores is non-empty…
for v in scores[1:] {
    if v < lo { lo = v }
    if v > hi { hi = v }
}

That loop is fine — until scores is empty, where scores[0] is a bounds violation you forgot to guard. The hand-roll is more code that you own and must keep correct, and it silently drops the empty-slice case the source already thought through for you. Reaching for slice.min_max isn't laziness; it's inheriting a decision someone already made carefully. The only reason you know it's there is that you read the file instead of guessing.

The counterfactual: if you genuinely needed a custom comparison the standard proc doesn't offer — say "smallest by a field, largest by another" — then the hand-rolled loop is the right call, and reading the source still pays: you'd find min_index / max_index two lines down and lift their shape. Reading first never wastes the trip.

Level 2 found one proc. Level 3 is the property that emerges from doing this for a week: each lookup leaves you with more than the one answer, and the well you're drawing from never runs dry.

Peripheral context is the bonus. You opened slice.odin for min_max, but reading the page around it, you passed its neighbors — and now you know they exist, which you never would have from a single handed-down answer. The cluster you walked through:

what sat next to min_max in core:slice/slice.odin

The well doesn't drain. A person's patience and a chat box's reliability both run out — and the chat box can confidently hand you Odin syntax that doesn't exist. The official overview, the package docs, and the core: source do not. They're the same files every time, they're authoritative, and reading them is the skill that keeps paying off long after you'd have exhausted anyone's goodwill.

The emergent payoff — the corpus grows by use. When a lookup was hard enough that you'd hate to redo it, you write the distilled answer back into the substrate (content/domains/odin/compiled/from-query/). Now the local search returns it for free next time, and so does future-you. Every answer you externalize is one you never re-derive — the habit doesn't just answer today's question, it compounds into a corpus that answers tomorrow's before you ask.

the trap this habit replaces The reflex worth unlearning is asking before reading. It feels faster and it isn't: for a syntax or signature question, a file already on disk beats a network round trip, and it can't hallucinate a proc that doesn't exist. Asking still has its place — after the cheap reading, when you can pose a sharp question ("the source says min_max wants an ordered element; my element is a struct — how do I order it?") instead of a vague one. You earn the right to ask by having read first.

That's the arc: L1 the loop — guess? then read the source, the real signature, call, verify → L2 run it once and you find slice.min_max, a three-return proc you'd never have guessed, that out-thinks your hand-rolled loop → L3 repeat it and the neighbors you read and the answers you write back compound into a corpus that never runs dry.

the min_max signature & outputs are real compiler output · probes in claims/lessons/19-search-driven-learning