For and While¶
Explicit iteration¶
for loops require an explicitly iterable value. A type is iterable only if
it implements or inherits from Iterable.
Advancing an iterator returns a result, the same way any other recoverable
outcome does (see Results): either the next
value, or a sentinel meaning there isn't one. Python signals this by
raising StopIteration — using an exception for the single most
ordinary outcome an iterator has, not an exceptional one. Lucid's iterators
return a value instead:
import iteration
type IterResult[T] = T | Literal[iteration.done]
trait Iterator[T]:
def next(self) -> IterResult[T]
__next__; Lucid drops the dunder in
favor of the ordinary name next — advancing an iterator by hand,
outside a for loop, is rare enough that it needs no builtin wrapping
it, the way len/abs wrap theirs. It stays plain self,
not the read-only self: ~Self
Read-only methods with ~Self
usually prefers: advancing an iterator moves it forward, an ordinary
mutation of the iterator's own position, even though it returns a
value the same way a read-only method would. iteration.done is a
singleton value, not a class — the same shape as none, just not
common enough to earn none's bare-word exemption in type position,
so it is written through Literal (see Literal types)
instead of getting a type invented to represent it. It matches like
any other case:
iteration.done lives in the iteration module rather than being a
builtin the way none is: none is a genuinely universal absence of a
value, needed everywhere, while iteration.done only matters to code
that implements or consumes the iterator protocol directly — a small
enough audience that an explicit import is the right cost, not a builtin
every program pays for.
It needs no bespoke definition either — it is an ordinary
Sentinel, the same well-identified singleton any other module can
reach for (see Name-captured identifiers):
Reversible and reversed¶
reversed(xs) needs xs to provide its own reverse-order iterator,
walked backward directly — not Iterable's forward __iter__ run in
reverse afterward, since not every iterable can be walked backward at
all (a value read once from a stream, say). Reversible names that
narrower promise:
list and range satisfy Reversible; a plain generator does
not — the same distinction reversed(x for x in xs) already runs
into today as a runtime TypeError, just caught ahead of the call
instead of during it.
Fresh loop bindings¶
Python's for loop reuses one binding across every iteration: the loop
target is a single variable, reassigned each time around, not a fresh one
per iteration. A closure created inside the loop body captures that same
variable, not its value at the moment of capture, so every closure ends up
seeing whatever the loop left it at when the loop finished, not the value
it appeared to capture:
fns = []
for i in [1, 2, 3]:
fns.append(def(): print(i))
for f in fns:
f() # 3 3 3 in Python -- every closure shares the one binding
basedpython: a closure created during one iteration keeps that
iteration's value, unaffected by any that follow.
This is the same fix JavaScript's let made to its own var-based
loops, for the same reason: a variable a reader expects to be scoped to
one iteration should behave that way, not leak its final value into every
closure that captured it. The rule applies wherever a loop introduces a
binding, comprehension targets included.
No loop else¶
Python loop else clauses run when a loop finishes without break. Lucid
removes loop else.
if_broken loop clauses¶
Lucid adds an optional if_broken clause for loops. if_broken is a
keyword. The if_broken suite runs if the loop exits by break. Ordinary
fall-through code handles the case where a loop exits normally without
break.
def find_match(items: Iterable[Item]) -> Item | none:
for item in items:
if is_match(item):
found = item
break
if_broken:
return found
return none
Nested if_broken clauses¶
In nested loops, an if_broken clause belongs to the loop immediately before
it. A break inside an inner loop can therefore be handled by the inner
loop's if_broken clause, and that clause can choose to break the outer loop:
Loop control statements¶
break exits the innermost loop. If that loop has an if_broken
clause, control enters that clause before continuing after the loop.
Without if_broken, control continues at the first statement after the
loop. break outside a loop is a compile-time error.
continue skips the rest of the current iteration of the innermost loop
and starts the next iteration. It does not run if_broken, because the
loop did not break. continue outside a loop is a compile-time error.
assert, del, and pass are also fully settled. assert is covered
on its own — see Assert. del is a compile-time error on
a declared field (see No del on fields), removed entirely
in favor of explicit methods on a mapping or sequence index (see
No delitem), and retained for exactly one purpose
beyond those: ending a local name's lifetime early.
pass keeps its ordinary Python meaning: a statement that does
nothing, standing in wherever the grammar requires a statement and the
author has none to write. That makes it the statement-level counterpart
to skip, an expression-level elision marker — the two
fill the same kind of gap one level apart, and neither replaces the
other.