Skip to content

Lua Iterators

An iterator is a mechanism that allows you to traverse through all elements of a collection (often a table in Lua) one by one. Lua’s generic for loop is the primary way to use iterators.

The generic for loop uses an iterator function to step through a collection. The syntax is:

for var_1, var_2, ..., var_n in iterator_function, state, initial_value do
-- loop body
end

Lua provides two commonly used built-in iterator functions for tables:

  • ipairs(t): Iterates over the integer keys of table t from 1 up to the first missing integer key (i.e., for array-like sequences). It returns the index (key) and value at each step.
  • pairs(t): Iterates over all key-value pairs in table t, including non-integer keys and non-sequential integer keys. The order of iteration is not guaranteed for pairs.

Example with ipairs (for sequences):

local days = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday"}
for index, dayName in ipairs(days) do
print(index, dayName)
end
-- Output:
-- 1 Monday
-- 2 Tuesday
-- 3 Wednesday
-- 4 Thursday
-- 5 Friday

Example with pairs (for general tables):

local person = { name = "Alice", age = 30, city = "New York" }
person[1] = "Extra Info" -- Add a numeric key
for key, value in pairs(person) do
print(key, value)
end
-- Possible Output (order might vary):
-- name Alice
-- city New York
-- 1 Extra Info
-- age 30

In Lua, iterators are implemented using functions. The generic for loop repeatedly calls this iterator function. There are two main types of custom iterators based on how they maintain state:

  • Stateless Iterators
  • Stateful Iterators

A stateless iterator is a function that, for each iteration, receives the current state and control variable from the for loop itself. It doesn’t need to keep any state internally across calls; the for loop manages the state.

Let’s create a stateless iterator that generates squares of numbers up to a limit.

-- Iterator function: square_iter(limit, current_num)
-- limit: the invariant state (max count)
-- current_num: the control variable
function square_iter(limit, current_num)
current_num = current_num + 1
if current_num <= limit then
return current_num, current_num * current_num -- returns new control_var, value(s)
end
-- No more values, return nil to stop iteration
end
-- Using the stateless iterator directly with the for loop
-- for <loop_vars> in <iterator_func>, <invariant_state>, <initial_control_var> do
print("Squares using direct iterator function:")
for i, n_squared in square_iter, 3, 0 do
print(i, n_squared)
end
-- Output:
-- 1 1
-- 2 4
-- 3 9

Often, you’ll wrap the iterator function in another function (a factory) that sets up the initial state for the for loop, similar to how ipairs and pairs work.

-- Factory function for the squares iterator
function squares_factory(limit)
-- Returns: iterator_function, invariant_state, initial_control_value
return square_iter, limit, 0
end
print("\nSquares using factory function:")
for i, n_squared in squares_factory(4) do
print(i, n_squared)
end
-- Output:
-- 1 1
-- 2 4
-- 3 9
-- 4 16

A stateful iterator maintains its state across calls using closures. A closure is a function that captures (closes over) its surrounding local variables (upvalues). To create a stateful iterator, you typically define a factory function that creates and returns the closure.

Let’s create an iterator for the elements of an array using a closure.

local fruits = {"Apple", "Banana", "Cherry"}
-- Factory function that returns the iterator (a closure)
function element_iterator_factory(collection)
local index = 0 -- Upvalue, state for the iterator
local count = #collection -- Upvalue, another piece of state
-- This is the closure, the actual iterator function
return function()
index = index + 1
if index <= count then
return collection[index] -- Return the next element
else
return nil -- No more elements, stop iteration
end
end
end
print("\nIterating fruits with stateful iterator:")
-- The for loop calls the closure returned by element_iterator_factory
for fruit in element_iterator_factory(fruits) do
print(fruit)
end
-- Output:
-- Apple
-- Banana
-- Cherry

In this example, element_iterator_factory is called once. It sets up index and count. The inner anonymous function (the closure) is returned and becomes the iterator. Each time the for loop calls this closure, it can access and modify the index upvalue from its enclosing scope, thus maintaining its state.

Practical Application: Iterators are used everywhere for processing collections. Custom iterators are powerful for abstracting complex data structures or sequences, like reading lines from a file (io.lines), traversing a tree, or generating values on demand.