← Volver al listado de tecnologías

Capítulo 14: Iteradores - Más que Loops

Por: Artiko
luaiteratorsiterationgeneric-for

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:

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)

2. Invariant State (estado invariante)

3. Control Variable (variable de control)

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:

  1. Python usa yield: El generador pausa su ejecución y la resume
  2. Lua usa closures: El estado se mantiene en variables locales capturadas
  3. Python es más expresivo: yield es más claro que retornar closures
  4. Lua es más flexible: El protocolo de 3 componentes permite optimizaciones

Ventajas de Lua:

Ventajas de Python:

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:

  1. Protocolo explícito: Iterator function, invariant state, control variable
  2. Stateless vs Stateful: Eficiencia vs flexibilidad
  3. Generic for: Azúcar sintáctica elegante sobre el protocolo
  4. Composición: Combina iteradores para crear pipelines de procesamiento
  5. 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.