Iterators & Closures
for in Nim is not a language primitive over indexes: it is a call to an iterator. Once you can write iterators yourself, every custom container becomes loop-ready, and lazy generation becomes a technique you reach for instead of building temporary sequences.
Writing Iterators
An iterator looks like a procedure with yield instead of return. Each yield hands one value to the loop body, then execution resumes on the next loop iteration — the iterator's local variables keep their state between yields.
The Inline Iterator
iterator countdown(n: int): int =
var i = n # state survives across yields
while i > 0:
yield i # hand 'i' to the loop, then continue here later
dec i
for value in countdown(3):
echo value # 3, then 2, then 1
# Restricting the consumer: 'break' stops the loop and abandons the iterator.
for value in countdown(10):
if value < 8: break # the remaining yields simply never happen
echo value # 10, 9, 8
This is an inline iterator: the compiler expands its body at each call site, so there is no call overhead and no allocation for the state. The price is that an inline iterator is not a value — you cannot store it in a variable or return it from a procedure.
The items Protocol
Any type becomes loopable by defining items for it. This is Nim's answer to __iter__ or Iterable, and it costs one small iterator.
type
Playlist = object
tracks: seq[string]
iterator items(playlist: Playlist): string =
## Defining 'items' is what makes 'for track in playlist' compile.
for track in playlist.tracks:
yield track
let party = Playlist(tracks: @["intro", "main", "outro"])
for track in party:
echo track # intro, main, outro
# 'pairs' is the companion for index + value over arrays and sequences:
for index, name in ["ada", "grace"].pairs:
echo index, " -> ", name # 0 -> ada, 1 -> grace
Yielding Mutable Values
The standard library pairs items with mitems: the mutable variant yields a var slot, so the loop body can write through it.
var values = @[1, 2, 3]
for item in values.mitems: # 'mitems' yields var int
item *= 10 # writes straight into the sequence
echo values # @[10, 20, 30]
# mpairs gives (index, var value) when you need both:
for index, item in values.mpairs:
item += index # add the position to each element
echo values # @[10, 21, 32]
Closure Iterators
A closure iterator is a compiled state machine you can store, pass around and resume later. It answers the opposite need: flexibility where the inline iterator offers speed.
Iterators as Values
proc countTo(n: int): iterator (): int =
## Returns a closure iterator capturing 'n' and its own counter.
return iterator (): int =
var i = 0
while i <= n:
yield i
inc i
let countTo3 = countTo(3) # the iterator is now an ordinary value
echo countTo3() # 0 — the counter starts at zero
echo countTo3() # 1
echo countTo3() # 2
for value in countTo3(): # resumes the same state machine where it paused
echo value # 3, then the iterator runs out of values
Each call to a closure iterator resumes where the previous yield stopped, exactly like the inline version — but the state lives on the heap, so the value can outlive the procedure that created it.
Lazy and Unbounded Sequences
iterator fibonacci(): int =
## Nothing is computed until the loop asks for the next value.
var a = 0
var b = 1
while true: # unbounded: the consumer decides where to stop
yield a
let next = a + b
a = b
b = next
for value in fibonacci():
if value > 50: break # the loop owns the termination condition
echo value # 0 1 1 2 3 5 8 13 21 34
An unbounded iterator plus a loop condition replaces "build a big sequence, then walk it". Memory stays constant, and the first result arrives immediately — the pattern that later makes streaming and pipeline code natural.
Iterators or sequtils?
import std/sequtils
let squares = (1 .. 5).toSeq.mapIt(it * it) # node-style: eager, builds a seq
echo squares # @[1, 4, 9, 16, 25]
echo squares.filterIt(it mod 2 == 1) # @[1, 9, 25]
# mapIt/filterIt are TEMPLATES, not iterators: the expression is inlined per
# element and the loop structure is yours. Use them for short pipelines on
# sequences; write a real iterator when the data is large, streamed or generated.
Iterators in Practice
Choose the shape first, then write the code. Most iterator problems are really design problems about who owns the termination condition.
Choosing an Approach
- Inline iterator — the default. Fast, no allocation, works in
for, cannot be stored. Use it for containers and computed sequences. - Closure iterator — when the traversal must be passed to another procedure, stored in an object, or resumed step by step. Expect heap state and a little overhead.
sequtilstemplate — a one-line transformation over an existing sequence. Eager: it materializes the result.- Plain loop — when there is no traversal to name. Not every loop deserves an iterator; adding one can hide the very logic the reader needs to see.
Review Checklist
- Define
items(andmitemswhen mutation is legitimate) so your type behaves like a built-in. - Keep state in locals, never in globals, so two loops over the same iterator cannot interfere.
- Document whether an iterator is finite; an unbounded one must state what ends the loop.
- Prefer
breakover an internal limit parameter — let the caller decide when enough is enough. - Do not hide side effects inside an iterator; a loop body that surprises the reader is a bug in the design.