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.
  • sequtils template — 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

  1. Define items (and mitems when mutation is legitimate) so your type behaves like a built-in.
  2. Keep state in locals, never in globals, so two loops over the same iterator cannot interfere.
  3. Document whether an iterator is finite; an unbounded one must state what ends the loop.
  4. Prefer break over an internal limit parameter — let the caller decide when enough is enough.
  5. Do not hide side effects inside an iterator; a loop body that surprises the reader is a bug in the design.