El `for` genérico por dentro: la tripleta que gobierna el bucle
La lista de expresiones del `for` genérico se evalúa una sola vez y se ajusta a cuatro valores: función iteradora, estado invariante, variable de control y valor de cierre. Qué hace exactamente el bucle en cada vuelta, por qué el nil del primer resultado es la condición de parada, y qué cambia cuando las variables de control se vuelven inmutables en Lua 5.5.
El for genérico tiene aspecto de azúcar sintáctico para recorrer colecciones y no lo es: es un protocolo, un contrato de cuatro valores que cualquier función puede cumplir sin pedirle permiso al lenguaje. Mientras se lea como una construcción mágica que ya sabe recorrer tablas, pairs seguirá siendo una caja negra y escribir un iterador propio parecerá arte oscuro reservado a quien haya leído el código fuente del intérprete. En cuanto se ve la expansión —una llamada, una comparación con nulo, una asignación a la variable de control— el mecanismo entero cabe en diez líneas de Lua ordinario y se vuelve derivable en lugar de memorizable. Esta lección hace esa disección: qué produce la lista de expresiones, qué papel tiene cada uno de los cuatro valores, en qué orden ocurren las cosas dentro de una vuelta y qué implica el cuarto valor, el de cierre, que Lua 5.4 añadió al protocolo y que casi nadie usa.
- Reescribir cualquier
forgenérico en su forma equivalente con un bucle explícito. - Nombrar los cuatro valores del protocolo y el papel exacto de cada uno.
- Justificar la condición de parada y la actualización de la variable de control.
- Aplicar el cuarto valor de cierre y la inmutabilidad de las variables de control de Lua 5.5.
Cuatro valores, no uno
La sintaxis del for genérico es una lista de nombres, la palabra in y una lista de expresiones. Esa lista se evalúa una sola vez, antes de la primera vuelta, y su resultado se ajusta a exactamente cuatro valores con las reglas de ajuste habituales: sobran los que sobran, y los que falten se rellenan con nulo.
Los cuatro tienen nombre y función:
La función iteradora
Se llama una vez por vuelta con dos argumentos y devuelve los valores que se asignarán a las variables del bucle. Es el único de los cuatro que debe ser llamable.
El estado invariante
Se pasa como primer argumento en todas las llamadas y el bucle jamás lo modifica. Suele ser la colección recorrida, pero puede ser cualquier valor, incluido nulo.
La variable de control
Se pasa como segundo argumento. Es el único de los tres que cambia: tras cada vuelta el bucle le asigna el primer valor devuelto por la función iteradora.
El valor de cierre
Cuarto valor, incorporado en Lua 5.4. El bucle lo trata como una variable de cierre, de modo que su metamétodo __close se ejecuta al terminar, salga el bucle como salga.
La forma canónica de recordar esto es escribir la expansión. El bucle
for k, v in explist do
cuerpo
end
es equivalente, hasta en los detalles que importan, a este bloque:
do
local f, s, ctrl, cerrador = explist -- ajuste a cuatro valores, una sola vez
local guardia <close> = cerrador -- Lua 5.4 y posteriores
while true do
local k, v = f(s, ctrl) -- una llamada por vuelta
if k == nil then break end -- condicion de parada
ctrl = k -- el primer valor pasa a ser el control
cuerpo -- k y v son locales de esta vuelta
end
end
Todo lo demás de este nivel son consecuencias de esas nueve líneas. Que pairs funcione, que ipairs se detenga donde se detiene, que un iterador sin estado sea barato y que uno con estado cueste un closure por bucle: nada de eso son reglas nuevas, son lecturas de esta expansión.
Qué ocurre en cada vuelta
El orden importa y conviene fijarlo. La evaluación de la lista de expresiones ocurre fuera del bucle: si escribes for k, v in pairs(t) do, la llamada a pairs se hace una vez, no una por vuelta, y lo que se repite es la llamada al valor que pairs devolvió. Confundir ambas cosas es el origen de la creencia de que pairs es cara.
flowchart TD A[Evaluar la lista de expresiones una sola vez] --> B[Ajustar el resultado a cuatro valores] B --> C[Llamar a la funcion iteradora con estado y control] C --> D[Recoger todos los valores devueltos] D --> E[Si el primero es nil terminar el bucle] D --> F[Si el primero no es nil pasa a ser el nuevo control] F --> G[Asignar los valores a las variables de esta vuelta] G --> H[Ejecutar el cuerpo] H --> C E --> I[Cerrar el cuarto valor si existe]
Tres consecuencias directas. La primera: la función iteradora puede devolver cuantos valores quiera, no dos; el bucle asigna a las variables declaradas y descarta el resto, exactamente como una llamada ordinaria. La segunda: solo el primer valor gobierna la parada, así que un iterador que quiera producir un nulo legítimo debe colocarlo en la segunda posición y nunca en la primera. La tercera: el estado invariante es una cortesía, no una obligación; hay iteradores que lo ignoran por completo y guardan todo lo que necesitan en un closure, y esa es justamente la distinción que ordena las lecciones tercera y cuarta de este nivel.
Si recorres una estructura cuyas claves pueden ser nulas, o si tu iterador devuelve un valor calculado que a veces sale nulo, el bucle terminará en silencio a mitad de recorrido y sin error. La solución idiomática es devolver un índice o una posición como primer valor y el dato como segundo, que es exactamente lo que hacen ipairs y next.
El cuarto valor: cerrar lo que el iterador abrió
Hasta Lua 5.3 el protocolo tenía tres valores y un problema sin resolver: un iterador que abriese un recurso —un fichero, un descriptor, una conexión— no tenía forma de soltarlo si el bucle terminaba antes de tiempo. Un break, un return desde dentro del cuerpo o un error dejaban el recurso colgando hasta que el recolector se acordara de él, con la impredecibilidad que eso implica.
Lua 5.4 añadió el cuarto valor. El bucle lo declara internamente como variable de cierre, de modo que su metamétodo __close se ejecuta al abandonar el bucle por cualquier vía, incluida la propagación de un error.
local function lineasDe(ruta)
local fh = assert(io.open(ruta, "r"))
local recurso = setmetatable({}, {
__close = function() fh:close() end,
})
return function() -- funcion iteradora
return fh:read("l")
end, nil, nil, recurso -- f, estado, control, cierre
end
for linea in lineasDe("datos.txt") do
if linea:match("^#") then break end -- el fichero se cierra igual
print(linea)
end
Nótese que aquí el estado invariante y la variable de control son ambos nulos: al iterador no le hacen falta porque toda su memoria vive en el upvalue fh. El protocolo lo tolera sin ceremonia, porque solo exige que el primer valor sea llamable.
Igual que en las variables de cierre escritas a mano, el cuarto valor puede ser nulo o falso y entonces el bucle no intenta cerrar nada. Por eso los iteradores clásicos de tres valores siguen funcionando sin cambios bajo 5.4 y 5.5: el ajuste rellena la cuarta posición con nulo y el mecanismo se desactiva solo.
Lua 5.5: las variables del bucle son de solo lectura
Un detalle del presente que reordena varios idiomas antiguos. En Lua 5.5 las variables de control de un bucle for, tanto del numérico como del genérico, son inmutables dentro del cuerpo: se pueden leer, y cualquier asignación es un error detectado al compilar.
La motivación se lee sola en la expansión de arriba. La variable k que ves en el cuerpo no es el control real del bucle, es una copia que la máquina virtual deposita en cada vuelta; el ctrl que gobierna la siguiente llamada vive en un registro interno que tu código no puede nombrar. Escribir en k nunca alteró el recorrido, así que el lenguaje ofrecía una operación cuyo efecto observable era casi siempre distinto del esperado.
-- Idioma antiguo que deja de compilar en Lua 5.5
for k, v in pairs(t) do
if type(k) == "string" then k = k:lower() end -- error: k es de solo lectura
print(k, v)
end
-- Reescritura correcta con una local propia
for k, v in pairs(t) do
local clave = k
if type(clave) == "string" then clave = clave:lower() end
print(clave, v)
end
El patrón afectado más frecuente es precisamente ese: normalizar dentro del cuerpo el valor que entrega el iterador. Se arregla con una declaración local, que además documenta que el valor normalizado es cosa tuya y no del recorrido.
Aquí está la idea que hay que llevarse, y es más radical de lo que parece: el for genérico no sabe recorrer colecciones. No conoce las tablas, no conoce los ficheros, no conoce las cadenas. Lo único que sabe hacer es llamar repetidamente a un valor con dos argumentos, mirar si el primer resultado es nulo y, si no lo es, guardarlo para la llamada siguiente. Todo el conocimiento sobre cómo se recorre algo está fuera del bucle, en la función que le entregues. Esto es exactamente el mismo movimiento de diseño que ya viste en las metatablas: en lugar de meter en el lenguaje una jerarquía de colecciones iterables, con su interfaz, sus clases base y su tabla de métodos, Lua define un contrato mínimo y deja que cualquiera lo cumpla. La consecuencia práctica es que la pregunta ¿puedo recorrer esto con un for? está mal planteada. La pregunta correcta es ¿puedo escribir una función que, llamada repetidamente, entregue los elementos de esto y termine devolviendo nulo?, y la respuesta es afirmativa para casi cualquier estructura imaginable: un árbol, un flujo de red, una secuencia infinita truncada, las coincidencias de un patrón, las líneas de un fichero, los resultados de una consulta remota. La consecuencia teórica es aún más interesante: el protocolo es un coiterador externo, es decir, el consumidor controla el ritmo y el productor es reactivo. Por eso un break funciona sin ceremonia, por eso el bucle puede abandonarse a la mitad y por eso hizo falta añadir un cuarto valor para limpiar. Compáralo con un recorrido interno del estilo pásame una función y yo la aplico a cada elemento: allí el productor manda, cortar el recorrido exige un mecanismo extra y componer dos recorridos es incómodo. Lua eligió el externo, con tres valores, y luego lo extendió a cuatro sin romper nada. Esa capacidad de crecer sin romper es lo que distingue un protocolo bien elegido de una interfaz impuesta.
- Reescribe a mano, con un bucle explícito y sin
for, un recorrido conpairssobre una tabla de cinco claves. Comprueba que imprime lo mismo. - Escribe una función iteradora que devuelva tres valores por vuelta y consúmela con un
forque declare solo dos variables. Explica adónde va el tercero. - Construye un iterador que devuelva nulo como segundo valor en algunas vueltas y verifica que el bucle no se detiene. Luego muévelo a la primera posición y observa el corte.
- Envuelve la apertura de un fichero en un iterador de cuatro valores, sal del bucle con
breaken la tercera línea y confirma con el manejador que quedó cerrado. - Toma un fragmento que asigne a la variable de control dentro del cuerpo y reescríbelo para que compile bajo las reglas de Lua 5.5.