Capítulo 14: Iteradores - Más que Loops
Capítulo 14: Iteradores - Más que Loops
Los iteradores en Lua son más que simples loops. Representan un protocolo elegante que permite recorrer colecciones de manera expresiva y eficiente. En este capítulo, exploraremos desde los fundamentos hasta técnicas avanzadas para crear tus propios iteradores.
1. El Protocolo de Iteración en Lua
El for genérico de Lua (for k, v in ...) funciona con un protocolo de tres componentes:
-- El for genérico tiene esta forma:
-- for var1, var2, ... in <expresión> do
-- <cuerpo>
-- end
-- <expresión> debe retornar 3 valores:
-- 1. iterator function (la función iteradora)
-- 2. invariant state (estado invariante)
-- 3. control variable (variable de control inicial)
-- Ejemplo manual del protocolo:
local function simple_iterator(state, control)
control = control + 1
if control <= state.max then
return control, state.data[control]
end
end
-- Función factory que retorna los 3 valores
local function make_iterator(t, max)
return simple_iterator, {data = t, max = max}, 0
end
-- Uso:
for i, v in make_iterator({10, 20, 30}, 3) do
print(i, v) -- 1 10, 2 20, 3 30
end
Cómo funciona internamente:
-- El for genérico es equivalente a:
do
local iter, state, control = <expresión>
while true do
local var1, var2, ... = iter(state, control)
control = var1 -- La primera variable se convierte en control
if control == nil then break end
<cuerpo>
end
end
2. Stateless Iterators (como ipairs)
Los iteradores sin estado no mantienen información entre llamadas. Solo dependen del estado invariante y la variable de control.
ipairs: El ejemplo clásico
-- Implementación simplificada de ipairs
local function iter(t, i)
i = i + 1
local v = t[i]
if v ~= nil then
return i, v
end
end
function myipairs(t)
return iter, t, 0
-- ^ ^ ^
-- | | +-- control inicial
-- | +-- state (la tabla)
-- +-- iterator function
end
-- Uso:
local frutas = {"manzana", "banana", "cereza"}
for i, fruta in myipairs(frutas) do
print(i, fruta)
end
pairs: Recorriendo todas las claves
-- pairs usa next internamente
function mypairs(t)
return next, t, nil
-- ^ ^ ^
-- | | +-- primera clave (nil)
-- | +-- la tabla
-- +-- next es la función iteradora
end
local persona = {nombre = "Ana", edad = 30, ciudad = "Lima"}
for k, v in mypairs(persona) do
print(k, v)
end
-- Salida (orden no garantizado):
-- nombre Ana
-- edad 30
-- ciudad Lima
Ventajas de stateless iterators:
- Más eficientes (menos overhead)
- No requieren closures
- Pueden reutilizarse fácilmente
3. Stateful Iterators con Closures
Los iteradores con estado mantienen información interna entre llamadas usando closures.
Ejemplo básico: contador
function contador(max)
local count = 0
return function() -- Iterator function (closure)
count = count + 1
if count <= max then
return count
end
end
end
-- Uso:
for n in contador(5) do
print(n) -- 1, 2, 3, 4, 5
end
Lector de líneas de archivo:
function lines_from_file(filename)
local file = io.open(filename, "r")
if not file then return function() end end
return function()
local line = file:read("*line")
if line == nil then
file:close()
return nil
end
return line
end
end
-- Uso:
for line in lines_from_file("config.txt") do
print(line)
end
Iterator con estado complejo:
-- Iterator que genera palabras de un string
function words(text)
local pos = 1
return function()
while pos <= #text do
local start_pos = pos
-- Buscar inicio de palabra
start_pos = text:find("%S", start_pos)
if not start_pos then return nil end
-- Buscar fin de palabra
local end_pos = text:find("%s", start_pos)
pos = end_pos and (end_pos + 1) or (#text + 1)
return text:sub(start_pos, (end_pos or #text + 1) - 1)
end
end
end
-- Uso:
for word in words("Hola mundo desde Lua") do
print(word)
end
-- Salida:
-- Hola
-- mundo
-- desde
-- Lua
4. Generic For: Cómo Funciona
El for genérico es azúcar sintáctica para un patrón específico:
-- Forma genérica:
for <vars> in <expr> do
<body>
end
-- Se expande a:
do
local _iter, _state, _control = <expr>
while true do
local <vars> = _iter(_state, _control)
_control = <primera_var>
if _control == nil then break end
<body>
end
end
Ejemplo práctico:
-- Iterator personalizado
function range(from, to, step)
step = step or 1
return function(_, current)
current = current + step
if (step > 0 and current <= to) or
(step < 0 and current >= to) then
return current
end
end, nil, from - step
-- ^ ^ ^
-- | | +-- control inicial
-- | +-- state (no necesitamos uno)
-- +-- iterator function
end
-- Uso:
for i in range(1, 10, 2) do
print(i) -- 1, 3, 5, 7, 9
end
for i in range(10, 1, -2) do
print(i) -- 10, 8, 6, 4, 2
end
5. Crear Iteradores Personalizados
Pattern: Stateless iterator
-- Iterar sobre valores filtrados
function filter_iter(t, predicate)
local function iter(state, control)
for i = control + 1, #state.data do
if state.predicate(state.data[i]) then
return i, state.data[i]
end
end
end
return iter, {data = t, predicate = predicate}, 0
end
-- Uso:
local numeros = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
for i, n in filter_iter(numeros, function(x) return x % 2 == 0 end) do
print(i, n) -- Solo pares: 2, 4, 6, 8, 10
end
Pattern: Stateful iterator con closure
-- Fibonacci iterator
function fibonacci(max)
local a, b = 0, 1
local count = 0
return function()
if count >= max then return nil end
count = count + 1
a, b = b, a + b
return count, a
end
end
-- Uso:
for i, fib in fibonacci(10) do
print(string.format("F(%d) = %d", i, fib))
end
Pattern: Iterator que transforma valores
-- Map iterator: transforma cada elemento
function map_iter(iterator, transform)
return function(state, control)
local results = {iterator(state, control)}
if results[1] == nil then return nil end
-- Transformar todos los valores retornados
for i = 2, #results do
results[i] = transform(results[i])
end
return table.unpack(results)
end
end
-- Uso con ipairs:
local numeros = {1, 2, 3, 4, 5}
for i, cuadrado in map_iter(ipairs(numeros), function(x) return x * x end) do
print(i, cuadrado) -- 1 1, 2 4, 3 9, 4 16, 5 25
end
6. Iteradores con Múltiples Valores
Los iteradores pueden retornar múltiples valores en cada iteración.
-- Iterator que retorna índice, valor y si es el último
function enumerate_with_last(t)
local n = #t
local function iter(state, i)
i = i + 1
if i <= n then
return i, state[i], i == n
end
end
return iter, t, 0
end
-- Uso:
local items = {"primero", "segundo", "tercero"}
for i, item, is_last in enumerate_with_last(items) do
print(string.format("[%d] %s%s", i, item, is_last and " (último)" or ""))
end
-- Salida:
-- [1] primero
-- [2] segundo
-- [3] tercero (último)
Iterator para pares de elementos:
-- Retorna pares consecutivos de elementos
function pairs_iter(t)
return function(state, i)
i = i + 1
if i < #state then
return i, state[i], state[i + 1]
end
end, t, 0
end
-- Uso:
local nums = {1, 2, 3, 4, 5}
for i, current, next_val in pairs_iter(nums) do
print(string.format("Par [%d]: %d, %d", i, current, next_val))
end
-- Salida:
-- Par [1]: 1, 2
-- Par [2]: 2, 3
-- Par [3]: 3, 4
-- Par [4]: 4, 5
7. DEEP DIVE: El Protocolo Completo
El protocolo de iteración de Lua tiene tres componentes clave:
1. Iterator Function (función iteradora)
- Recibe:
(invariant_state, control_variable) - Retorna:
next_control, value1, value2, ... - Cuando retorna
nil, la iteración termina
2. Invariant State (estado invariante)
- Datos que no cambian durante la iteración
- Generalmente la colección o datos de configuración
- Puede ser
nilsi no se necesita
3. Control Variable (variable de control)
- Valor que cambia en cada iteración
- El primer valor retornado por el iterator se convierte en el nuevo control
- Generalmente es el índice o clave actual
Ejemplo completo:
-- Iterator avanzado con los 3 componentes explícitos
function sliding_window(t, size)
-- State: tabla original y tamaño de ventana
local state = {
data = t,
window_size = size,
total = #t
}
-- Iterator function
local function iter(st, position)
position = position + 1
-- Verificar si hay suficientes elementos
if position + st.window_size - 1 > st.total then
return nil
end
-- Construir ventana
local window = {}
for i = 0, st.window_size - 1 do
window[i + 1] = st.data[position + i]
end
return position, window
end
-- Retornar: iterator, state, control inicial
return iter, state, 0
end
-- Uso:
local datos = {1, 2, 3, 4, 5, 6}
for pos, window in sliding_window(datos, 3) do
print(string.format("Posición %d:", pos), table.concat(window, ", "))
end
-- Salida:
-- Posición 1: 1, 2, 3
-- Posición 2: 2, 3, 4
-- Posición 3: 3, 4, 5
-- Posición 4: 4, 5, 6
¿Por qué tres componentes?
-- Permite reutilizar la misma función iteradora con diferentes estados
local function common_iter(state, control)
control = control + state.step
if control <= state.max then
return control
end
end
-- Diferentes "instancias" usando el mismo iterator
local iter1 = function() return common_iter, {step = 1, max = 5}, 0 end
local iter2 = function() return common_iter, {step = 2, max = 10}, 0 end
print("Iterator 1:")
for n in iter1() do
print(n) -- 1, 2, 3, 4, 5
end
print("Iterator 2:")
for n in iter2() do
print(n) -- 2, 4, 6, 8, 10
end
8. SOAPBOX: Iteradores vs Generadores de Python
Si vienes de Python, notarás diferencias significativas en cómo Lua maneja la iteración:
Python Generators:
def fibonacci(max):
a, b = 0, 1
count = 0
while count < max:
yield a
a, b = b, a + b
count += 1
for n in fibonacci(10):
print(n)
Lua Stateful Iterator (equivalente):
function fibonacci(max)
local a, b = 0, 1
local count = 0
return function()
if count >= max then return nil end
local result = a
a, b = b, a + b
count = count + 1
return result
end
end
for n in fibonacci(10) do
print(n)
end
Diferencias clave:
- Python usa
yield: El generador pausa su ejecución y la resume - Lua usa closures: El estado se mantiene en variables locales capturadas
- Python es más expresivo:
yieldes más claro que retornar closures - Lua es más flexible: El protocolo de 3 componentes permite optimizaciones
Ventajas de Lua:
- Los stateless iterators son más eficientes que generators
- Puedes retornar múltiples valores fácilmente
- El protocolo explícito da más control
Ventajas de Python:
yieldes más intuitivo y legible- Generators automáticamente manejan el estado
- Syntax más limpia para casos comunes
Mi opinión: Python gana en ergonomía para iteradores complejos, pero Lua gana en eficiencia con stateless iterators. El protocolo de Lua es más “a mano”, pero esa flexibilidad vale la pena cuando optimizas.
9. Caso Práctico: Iterator para Árbol de Archivos
Vamos a crear un iterator que recorra un directorio recursivamente (simulado):
-- Simular sistema de archivos
local filesystem = {
{
name = "src",
type = "dir",
children = {
{name = "main.lua", type = "file", size = 1024},
{name = "utils.lua", type = "file", size = 512},
{
name = "lib",
type = "dir",
children = {
{name = "parser.lua", type = "file", size = 2048},
{name = "lexer.lua", type = "file", size = 1536}
}
}
}
},
{
name = "tests",
type = "dir",
children = {
{name = "test_main.lua", type = "file", size = 768}
}
},
{name = "README.md", type = "file", size = 256}
}
-- Iterator con DFS (depth-first search)
function walk_tree(tree)
-- Stack para mantener estado de la búsqueda
local stack = {}
-- Inicializar stack con elementos del nivel raíz
for i = #tree, 1, -1 do
table.insert(stack, {
node = tree[i],
depth = 0,
path = tree[i].name
})
end
return function()
if #stack == 0 then return nil end
-- Pop del stack
local current = table.remove(stack)
local node = current.node
local depth = current.depth
local path = current.path
-- Si es directorio, agregar hijos al stack (en reversa)
if node.type == "dir" and node.children then
for i = #node.children, 1, -1 do
local child = node.children[i]
table.insert(stack, {
node = child,
depth = depth + 1,
path = path .. "/" .. child.name
})
end
end
return path, node.type, node.size, depth
end
end
-- Uso:
print("Estructura de archivos:")
for path, type, size, depth in walk_tree(filesystem) do
local indent = string.rep(" ", depth)
local info = type == "file" and string.format(" (%d bytes)", size) or ""
print(string.format("%s%s%s", indent, path, info))
end
Salida:
Estructura de archivos:
src
src/main.lua (1024 bytes)
src/utils.lua (512 bytes)
src/lib
src/lib/parser.lua (2048 bytes)
src/lib/lexer.lua (1536 bytes)
tests
tests/test_main.lua (768 bytes)
README.md (256 bytes)
Versión BFS (breadth-first search):
function walk_tree_bfs(tree)
-- Cola FIFO
local queue = {}
for _, node in ipairs(tree) do
table.insert(queue, {
node = node,
depth = 0,
path = node.name
})
end
return function()
if #queue == 0 then return nil end
-- Dequeue (remover del inicio)
local current = table.remove(queue, 1)
local node = current.node
local depth = current.depth
local path = current.path
-- Si es directorio, agregar hijos al final de la cola
if node.type == "dir" and node.children then
for _, child in ipairs(node.children) do
table.insert(queue, {
node = child,
depth = depth + 1,
path = path .. "/" .. child.name
})
end
end
return path, node.type, node.size, depth
end
end
-- Comparar BFS vs DFS
print("\nBFS (nivel por nivel):")
for path, type, size, depth in walk_tree_bfs(filesystem) do
print(string.format("[Nivel %d] %s", depth, path))
end
Iterator con filtros:
-- Iterator que solo retorna archivos de cierto tipo
function find_files(tree, extension)
local base_iter = walk_tree(tree)
return function()
while true do
local path, type, size, depth = base_iter()
if path == nil then return nil end
-- Filtrar solo archivos con la extensión correcta
if type == "file" and path:match("%." .. extension .. "$") then
return path, size, depth
end
end
end
end
-- Uso:
print("\nArchivos .lua:")
for path, size, depth in find_files(filesystem, "lua") do
print(string.format("%s (%d bytes)", path, size))
end
10. Ejercicios
Ejercicio 1: Iterator de Chunks
Crea un iterator chunks(t, size) que divida una tabla en sub-tablas de tamaño size:
-- Tu implementación aquí:
function chunks(t, size)
-- TODO: implementar
end
-- Caso de prueba:
local datos = {1, 2, 3, 4, 5, 6, 7, 8, 9}
for chunk_num, chunk in chunks(datos, 3) do
print(string.format("Chunk %d:", chunk_num), table.concat(chunk, ", "))
end
-- Salida esperada:
-- Chunk 1: 1, 2, 3
-- Chunk 2: 4, 5, 6
-- Chunk 3: 7, 8, 9
Bonus: Maneja el caso donde el último chunk es más pequeño.
Ejercicio 2: Iterator Combinador
Implementa zip(iter1, iter2) que combine dos iteradores retornando pares de valores:
-- Tu implementación aquí:
function zip(...)
-- TODO: implementar para N iteradores
end
-- Caso de prueba:
local numeros = {1, 2, 3, 4}
local letras = {"a", "b", "c", "d"}
for i, num, letra in zip(ipairs(numeros), ipairs(letras)) do
print(string.format("%d: %d -> %s", i, num, letra))
end
-- Salida esperada:
-- 1: 1 -> a
-- 2: 2 -> b
-- 3: 3 -> c
-- 4: 4 -> d
Pista: Necesitas almacenar múltiples estados de iteración.
Ejercicio 3: Iterator Lazy Infinite
Crea un iterator infinito naturals() que genere números naturales, y luego implementa take(iterator, n) que tome solo los primeros n elementos:
-- Tu implementación aquí:
function naturals(start)
start = start or 1
-- TODO: implementar
end
function take(iterator, n)
-- TODO: implementar
end
-- Caso de prueba:
for i, n in take(naturals(100), 5) do
print(i, n)
end
-- Salida esperada:
-- 1 100
-- 2 101
-- 3 102
-- 4 103
-- 5 104
Bonus: Implementa takeWhile(iterator, predicate) que tome elementos mientras la condición sea verdadera.
Soluciones
Solución Ejercicio 1: Iterator de Chunks
function chunks(t, size)
local total = #t
local function iter(state, pos)
pos = pos + size
if pos > state.total then return nil end
local chunk = {}
for i = 0, state.size - 1 do
local idx = pos + i
if idx <= state.total then
chunk[i + 1] = state.data[idx]
end
end
return pos, chunk
end
return iter, {data = t, size = size, total = total}, 1 - size
end
Solución Ejercicio 2: Iterator Combinador
function zip(...)
local iterators = {...}
local states = {}
local controls = {}
-- Separar iterators, states y controls
for i = 1, #iterators, 3 do
table.insert(states, {
iter = iterators[i],
state = iterators[i + 1],
control = iterators[i + 2]
})
end
return function()
local results = {}
for i, st in ipairs(states) do
local vals = {st.iter(st.state, st.control)}
if vals[1] == nil then return nil end
st.control = vals[1]
for j = 1, #vals do
table.insert(results, vals[j])
end
end
return table.unpack(results)
end
end
Solución Ejercicio 3: Iterator Lazy Infinite
function naturals(start)
start = start or 1
local n = start - 1
return function()
n = n + 1
return n
end
end
function take(iterator, n)
local count = 0
return function()
count = count + 1
if count > n then return nil end
local val = iterator()
return count, val
end
end
-- Bonus: takeWhile
function takeWhile(iterator, predicate)
return function()
local val = iterator()
if val == nil or not predicate(val) then
return nil
end
return val
end
end
Resumen
Los iteradores en Lua son una característica poderosa que permite:
- Protocolo explícito: Iterator function, invariant state, control variable
- Stateless vs Stateful: Eficiencia vs flexibilidad
- Generic for: Azúcar sintáctica elegante sobre el protocolo
- Composición: Combina iteradores para crear pipelines de procesamiento
- Flexibilidad: Múltiples valores de retorno, iteradores infinitos, filtros
Dominar los iteradores te permitirá escribir código más expresivo y eficiente en Lua.
En el próximo capítulo exploraremos Garbage Collection y Weak Tables, entendiendo cómo Lua maneja la memoria automáticamente.