Iteration & Loops
Most iteration in Nomi happens through Iter module functions —
Iter.map, Iter.filter, Iter.reduce, the pipe-friendly shapes the
last few chapters used. For stateful loops that don’t fit that pattern,
Iter.loop(|state = init| body) is the primitive.
Five control statements show up in this chapter. First, Nomi’s return:
this isn’t iteration-specific, but the rules are worth pinning down before
we hit iteration callbacks below. return is lambda-scoped — return v
inside any lambda exits that lambda with v. There is no non-local return.
Two forms:
return— exit the current lambda withUnit.return value— exit the current lambda withvalue.
Inside iteration callbacks — any lambda passed to one of the
iteration functions (Iter.loop, Iter.map, Iter.reduce, and the
rest of the iter family) — three more statements come into
play, and return/return value take on iteration-specific semantics
(exit the callback; the iteration moves on to the next element). All five:
return— exit the callback withUnitas the result for this iteration. The iteration uses that result however its operation calls for (mapwould putUnitin the output list at this position, for example), and moves on to the next.return value— same as above but withvalueinstead ofUnit(mapputsvaluein the output,reducemakesvaluethe new accumulator,filtertreatsvalueas the Bool decision).continue— skip the current iteration entirely; the callback contributes nothing (mapomits it from the output,reduceleaves the accumulator unchanged) and the iteration moves on to the next.break— stop the iteration. The result depends on the iteration function (e.g. stdlib’sreducereturns the accumulator so far,mapandfilterreturn the items collected,eachandIter.loopreturnUnit).break value—return valueand stop:valueplays the same per-operation role it would as a normal result (reducemakes it the result,mapemits it as the final element,filterapplies it as the Bool decision for the current element —break Truekeeps it,break Falsedrops it), then iteration stops.
The Iter.loop primitive
Section titled “The Iter.loop primitive”loop lives in std/iter with the rest of the iteration machinery; the
canonical spelling is Iter.loop(...). (If you prefer the bare name,
import std/iter: Iter.loop brings it into scope like any other free
function.) It takes a lambda that carries state. Each iteration returns
either the next state (continue looping) or break value (exit and
yield the value). The state can be a single binding or a tuple:
fn main(): Int {
// Countdown — break when n reaches 0.
countdown = Iter.loop(|n = 5| {
if n == 0 { break n }
n - 1
})
dbg countdown
// Tuple state — first Fibonacci pair where the second is > 100.
(a, b) = Iter.loop(|state = (0, 1)| {
(x, y) = state
if y > 100 { break (x, y) }
(y, x + y)
})
dbg a
dbg b
}
break and continue in iter callbacks
Section titled “break and continue in iter callbacks”The same control-flow keywords work inside the lambdas you pass to
Iter.map / Iter.filter / Iter.reduce and friends. break stops the
iteration early; continue skips the current element:
fn main(): List<Int> {
// break in reduce — stop accumulating once total would exceed 60.
partial_sum =
[10, 20, 30, 40, 50]
|> Iter.reduce(|acc = 0, x| {
if acc + x > 60 { break }
acc + x
})
dbg partial_sum
// continue in map — skip odd numbers from the output.
evens_doubled =
[1, 2, 3, 4, 5]
|> Iter.map(|x| {
if x % 2 != 0 { continue }
x * 2
})
|> Iter.to_list()
dbg evens_doubled
}
return is lambda-scoped
Section titled “return is lambda-scoped”Inside a lambda, return v exits that lambda — not the enclosing
function. For iter callbacks, this is an early-exit-from-this-iteration
move: the iterator continues with the next element. This is the same
scoping rule that makes try on a Result inside a lambda bubble to the
lambda’s own boundary, not the enclosing fn.
fn main(): List<Int> {
// `return 300` exits the map callback's lambda; the map keeps going
// and the value lands in the output list at this position.
mapped =
[1, 2, 3, 4, 5]
|> Iter.map(|x| {
if x == 3 { return 300 }
x * 10
})
|> Iter.to_list()
dbg mapped
}
Nomi has no non-local returns. If you want to short-circuit the whole
iteration — exit from fn main based on something the callback saw — you
break from the callback and inspect the result outside.
Going deeper
Section titled “Going deeper”How iteration works in Nomi
Section titled “How iteration works in Nomi”Iteration is built on three primitives, and everything composes from these:
- The
Iterableinterface — pure protocol: implementnext(returnsMaybe<('T, Self)>) on your type (and optionally overrideknown_countif it stores its count), and everyItermodule function (map,filter,reduce,take_while,find,count, …) applies to values of your type without further wiring. Iter.loop— general state-threading; any imperative-style loop with custom termination logic fits.- Composing iter ops via pipes — chain
Iter.filter,Iter.take_while,Iter.reduce, and the rest to build whatever data-flow shape you need.
The stdlib uses these primitives the same way users do — the strict,
List-returning form is just Iter.map(xs, f) |> Iter.to_list(), the lazy
adapter plus an explicit materialize step; there are no eager adapter
functions on the collections. There’s no separate “engine” the user would
write that the stdlib doesn’t already provide.
The control signals
Section titled “The control signals”The five iteration control statements (return, return value, break,
break value, continue) are the whole callback-control protocol. They are
not Nomi values, and there is nothing to import, construct, or pattern-match.
What each keyword means inside an iter callback is the five-bullet list
at the top of this chapter. The one case those bullets don’t spell out is
the no-keyword one: a callback body that just produces a value the
regular way (its last expression) signals “here’s my result for this
element.” That’s what makes |n| n * 2 (no return) interchangeable
with |n| return n * 2 — they signal the same thing.
No Go panics
Section titled “No Go panics”Control signals propagate as ordinary return values inside the runtime, not via exceptions or Go panics. The iteration function inspects the callback’s signal and decides what to do (advance, skip, terminate). No implicit stack unwinding, no recover, no hidden stack walking — iteration is cheap and predictable.
Where break and continue are allowed
Section titled “Where break and continue are allowed”break and continue are valid inside callbacks passed to the standard iter
family: Iter.loop, Iter.map, Iter.reduce, Iter.filter, and the rest of
the operations built on Iter. Calls anywhere else reject break/continue
with a clear diagnostic.
That reflects the three-primitive model above: those are the iter callsites. Compose them when you need a new iteration shape.