Skip to content
AgileLogics
5 min read

Iterators

Prerequisites: Closures

A generic for loop works with any iterator — a function that returns the next value each time it's called, and nil when done.

Closure-based iterators

local function range(from, to, step)
  step = step or 1
  local current = from - step
  return function()
    current = current + step
    if current <= to then return current end
  end
end

for n in range(1, 10, 3) do io.write(n, " ") end
print()

local function words(s)
  return s:gmatch("%a+")
end
for w in words("iterators are neat") do print(w) end
1 4 7 10
iterators
are
neat

The iterator keeps its position in a closure variable (current).

Iterating over a table in sorted order

local function sortedPairs(t)
  local keys = {}
  for k in pairs(t) do keys[#keys + 1] = k end
  table.sort(keys)
  local i = 0
  return function()
    i = i + 1
    local k = keys[i]
    if k ~= nil then return k, t[k] end
  end
end

local stock = {pears = 0, apples = 10, plums = 4}
for fruit, count in sortedPairs(stock) do
  print(fruit, count)
end
apples	10
pears	0
plums	4

A reusable fix for pairs' unpredictable order.

How the generic for works

for vars in explist do evaluates explist to three values — an iterator function, a state, and an initial control value — then calls iterator(state, control) until it returns nil. ipairs uses this "stateless" form:

local function evens(t, i)
  i = i + 2
  if t[i] ~= nil then return i, t[i] end
end
local function everyOther(t) return evens, t, -1 end

for i, v in everyOther({"a", "b", "c", "d", "e"}) do
  print(i, v)
end
1	a
3	c
5	e

The control value starts at -1, so the first call returns index 1, then 3, then 5. Stateless iterators avoid creating a closure per loop, which matters in hot code.