wandres.dev
ITERADORES · el protocolo del for

`pairs` e `ipairs`: qué devuelve realmente cada uno

`pairs` no recorre nada: devuelve la tripleta formada por `next`, la tabla y nulo. `ipairs` tampoco: fabrica un contador que pregunta por índices consecutivos y se rinde en el primer hueco. Por qué el orden de `pairs` es indefinido por construcción, qué garantiza y qué no, y cómo se comportan ambos ante metatablas.

⏱ 17 min

Las dos funciones que todo el mundo escribe a diario son también las dos peor entendidas de la biblioteca básica, y la razón es que su nombre sugiere una acción —recorrer— que no realizan. Ni pairs ni ipairs recorren tabla alguna: ambas se limitan a devolver los tres valores que el for genérico necesita y a desaparecer del mapa antes de la primera vuelta. Toda la conducta observable del bucle, incluidas las dos preguntas que más veces se hacen en cualquier foro de Lua —por qué el orden cambia y por qué el recorrido se detiene antes de tiempo— está determinada por esos tres valores y por nada más. Esta lección abre las dos funciones, presenta a next como el iterador primitivo del que todo cuelga, y explica por qué la indefinición del orden no es un descuido de la implementación sino una consecuencia inevitable de cómo se guarda una tabla.

🎯 Al terminar esta lección sabrás
  • Enunciar con exactitud la tripleta que devuelve pairs y la que devuelve ipairs.
  • Explicar next como iterador primitivo y qué garantías ofrece durante una modificación.
  • Derivar la indefinición del orden de pairs desde la representación dual de la tabla.
  • Predecir dónde se detiene ipairs en una tabla con huecos y ante una metatabla.

pairs devuelve next, la tabla y nulo

La definición completa de pairs, sin metatablas de por medio, cabe en una línea de Lua:

local function pairs(t)
  return next, t, nil
end

Eso es todo. La función iteradora es next, el estado invariante es la tabla y la variable de control inicial es nulo. La expansión de la lección anterior hace el resto: llama a next(t, nil) para obtener el primer par, guarda la clave devuelta como nuevo control, llama a next(t, clave) para el siguiente, y termina cuando next devuelve nulo.

next es una función de la biblioteca básica con un contrato preciso. Recibe una tabla y una clave presente en ella, y devuelve la siguiente clave junto con su valor, en un orden no especificado; recibiendo nulo devuelve el primer par; y devuelve nulo cuando ya no quedan claves. Consultarla con una clave que no está en la tabla es un error.

local t = { a = 1, b = 2 }
print(next(t))            -- primer par, en el orden que decida la tabla
print(next(t, "a"))       -- el par que sigue a la clave a
print(next({}))           -- nil, la tabla vacia no tiene primer par

Que pairs sea trivial tiene una consecuencia práctica que conviene interiorizar: no cuesta nada. La creencia de que pairs es más lenta que ipairs porque construye algo es falsa; lo que cuesta es next, que en cada llamada debe localizar la posición de la clave recibida y avanzar hasta la siguiente ranura ocupada.

💡
Recorrer con next a mano es legítimo y a veces más claro

Nada obliga a pasar por pairs. Escribir for k, v in next, t do es exactamente el mismo bucle, ahorra una llamada y hace visible la tripleta; el ajuste rellena con nulo la variable de control. Es un idioma común en código que quiere ser explícito sobre lo que ocurre, y también la forma de saltarse un __pairs que no te interesa.

Por qué el orden es indefinido

La tabla de Lua tiene dos mitades: una parte de vector para las claves enteras densas y una parte de dispersión para todo lo demás. next recorre primero la parte de vector en orden de índice y luego la parte de dispersión en orden de ranura física, que es donde entra el azar.

La posición de una clave en la parte de dispersión depende de su valor de dispersión, del tamaño actual del vector interno y de las colisiones que hubo mientras se insertaba. Ninguno de esos tres factores es una propiedad del programa: el tamaño cambia con los rehashes, y el valor de dispersión de una cadena depende de una semilla que las versiones modernas de Lua aleatorizan al arrancar el estado precisamente para dificultar los ataques por colisiones.

flowchart TD
A[Llamada a next con una clave] --> B[Localizar la clave en la tabla]
B --> C[Si esta en la parte de vector avanzar al siguiente indice ocupado]
B --> D[Si esta en la parte de dispersion avanzar a la siguiente ranura ocupada]
C --> E[Al agotar el vector continuar por la parte de dispersion]
D --> F[Al agotar las ranuras devolver nil]
E --> D

De ahí se sigue algo más fuerte que el orden no está garantizado: dos ejecuciones del mismo programa sobre la misma tabla pueden producir órdenes distintos, y dos tablas con las mismas claves insertadas en distinto orden casi con seguridad los producirán. Cualquier código que dependa del orden de pairs es correcto por accidente, y la forma de dejar de depender de él es recoger las claves, ordenarlas con un criterio explícito y recorrer esa lista.

⚠️
Modificar la tabla durante el recorrido

Durante un recorrido con next puedes asignar nulo a campos existentes, incluido el que acabas de visitar, y borrarlos. Lo que no puedes es añadir claves nuevas: el comportamiento queda indefinido, porque una inserción puede provocar un rehash que reordene las ranuras bajo los pies del iterador y haga que se salten o se repitan entradas. Asignar un valor nuevo a una clave que ya existía sí es seguro, porque no cambia la estructura.

ipairs fabrica un contador y para en el primer hueco

ipairs no usa next ni recorre la tabla: cuenta. Su equivalente en Lua es igual de corto y explica todo lo que hace:

local function iterador(t, i)
  i = i + 1
  local v = t[i]
  if v ~= nil then return i, v end
end

local function ipairs(t)
  return iterador, t, 0
end

La variable de control es un entero que empieza en cero, y cada vuelta pregunta por el índice siguiente. Cuando la consulta devuelve nulo, el iterador no devuelve nada, el primer valor recogido por el bucle es nulo y el recorrido termina. De ahí las dos propiedades más citadas: el orden está garantizado, porque los índices se generan en sucesión, y el recorrido se detiene en el primer agujero, porque la ausencia de un índice es indistinguible del final de la secuencia.

local t = { 10, 20, nil, 40 }
for i, v in ipairs(t) do print(i, v) end   -- imprime 1 y 2, y se detiene

Que esto sea un defecto o una virtud depende de la disciplina del programa. Si la tabla es una secuencia sin huecos —el contrato habitual—, ipairs es el recorrido correcto y su parada es exactamente el final. Si la tabla tiene huecos, no hay recorrido correcto posible, porque ni siquiera el operador de longitud está definido en ese caso; el problema es la estructura, no la función.

🧮

ipairs respeta las metatablas

Desde Lua 5.3 la consulta interna es un acceso ordinario, así que dispara __index. Un proxy vacío con __index que sirva elementos calculados se recorre con ipairs sin haber almacenado nada.

🚫

El metametodo de ipairs ya no existe

Lua 5.2 introdujo un metamétodo para personalizar ipairs y Lua 5.3 lo eliminó. Personalizar el recorrido por índices se hace hoy con __index, que es más general y no añade una clave más al vocabulario de metatablas.

📏

No consulta la longitud

ipairs nunca evalúa el operador de longitud, así que no le afecta un __len ni la ambigüedad de las tablas con huecos. Solo pregunta por índices consecutivos hasta que uno falta.

⚖️

pairs ve las claves enteras tambien

Un recorrido con pairs visita las claves enteras junto con las demás, normalmente primero por el orden interno, pero eso es una casualidad de la implementación y no una garantía sobre la que construir.

Conviene añadir la nota del presente: bajo Lua 5.5 las variables que ambos recorridos entregan al cuerpo son inmutables, de modo que el viejo hábito de reasignar la clave o el índice dentro del bucle deja de compilar. En el caso de ipairs el cambio es especialmente saludable, porque reasignar el índice dentro del cuerpo nunca alteró el conteo y sin embargo aparentaba hacerlo.

Elegir con criterio

Las tres formas habituales de recorrer una tabla no son intercambiables, y la elección se decide por lo que garantiza cada una, no por su velocidad relativa.

Aspecto pairs ipairs for numérico hasta la longitud
Qué visita Todas las claves presentes Índices consecutivos desde uno Índices consecutivos desde uno
Orden Indefinido Garantizado Garantizado
Ante un hueco Sigue con las demás claves Se detiene Entrega un valor nulo
Consulta la longitud No No Sí, con su ambigüedad
Respeta __index No, va por la tabla real
Coste por vuelta Localizar y avanzar ranura Suma y consulta Suma y consulta

La lectura práctica es sencilla. Si la estructura es un diccionario, pairs es la única opción, y si además el orden importa hay que recoger las claves y ordenarlas explícitamente. Si es una secuencia sin huecos, ipairs expresa esa intención y se detiene sola donde debe. Y el for numérico solo aporta algo cuando necesitas el índice para algo más que leer el elemento —recorrer al revés, saltar de dos en dos, escribir mientras recorres— o cuando quieres distinguir un hueco de un final, porque a diferencia de ipairs te entregará el nulo en lugar de callarse.

Queda un caso que confunde a mucha gente: contar entradas. El operador de longitud no cuenta, informa de un borde de secuencia, y en una tabla con claves de cadena vale cero por definición. Contar exige recorrer con pairs y sumar, y el hecho de que no exista una función de biblioteca que lo haga no es un olvido: es la misma decisión de siempre, ya que mantener un contador actualizado encarecería toda inserción y todo borrado de todas las tablas del programa.

El orden no está indefinido por descuido: está indefinido porque no existe

La pregunta que hay que dejar de hacerse es por qué Lua no me garantiza el orden de pairs, porque presupone que ese orden existe en algún sitio y que el lenguaje se niega a revelarlo. No existe. Una tabla de Lua es una función parcial de claves a valores, y una función parcial no tiene orden; el orden que percibes al recorrerla es un artefacto de la manera concreta en que las ranuras físicas quedaron ocupadas esta vez, con esta semilla y tras estos rehashes. Garantizar un orden significaría añadir estructura que hoy no está: una lista enlazada de inserción, un árbol equilibrado o un vector de claves paralelo, y cualquiera de las tres cuesta memoria en todas las tablas del programa para beneficiar a las poquísimas que se recorren esperando un orden. Ese cálculo es el mismo que ha gobernado cada decisión de diseño de Lua desde el principio, y explica por qué otros lenguajes tomaron el camino contrario: cuando el diccionario es la estructura central de un lenguaje grande, y su recorrido ordenado es una expectativa cultural, pagar el enlace de inserción sale a cuenta. En Lua no sale, porque la tabla también es el array, también es el objeto, también es el módulo y también es el entorno, y encarecer una estructura tan universal para arreglar un caso de uso minoritario sería exactamente el tipo de negociación que el lenguaje se ha pasado treinta años rechazando. La consecuencia operativa es sencilla y no admite matices: si el orden importa, el orden es tuyo. Recoges las claves, las ordenas con el criterio que tu programa necesite y recorres esa lista. Y si esa operación aparece en muchos sitios, escribes un iterador que la encapsule, que es justamente lo que enseñan las tres lecciones siguientes.

📝
Lo esencial

pairs devuelve next, la tabla y nulo, salvo que exista un metamétodo que la sustituya; next avanza por la parte de vector y luego por la de dispersión, en orden de ranura física, y por eso el orden es indefinido en un sentido fuerte: no existe, no está oculto. Durante el recorrido puedes borrar y reasignar, pero no insertar claves nuevas. ipairs no usa next: fabrica un contador que arranca en cero, pregunta por índices consecutivos respetando __index y se detiene en cuanto uno falta, sin consultar nunca la longitud. Y en Lua 5.5 las variables que ambos entregan al cuerpo son de solo lectura.

⚔️ Mide, ordena y rompe
  1. Crea una tabla con diez claves de cadena, recórrela con pairs en dos ejecuciones distintas del intérprete y compara los órdenes.
  2. Reimplementa pairs e ipairs con los nombres misPairs y misIpairs y sustitúyelas en un programa real. Comprueba que nada cambia.
  3. Escribe un for genérico que use next directamente, sin pasar por pairs, y explica qué rellena la variable de control.
  4. Construye una tabla con un hueco en la posición tres y compara lo que ven ipairs, pairs y el operador de longitud.
  5. Escribe una función que devuelva un iterador de claves ordenadas alfabéticamente y úsala donde antes usabas pairs.