wandres.dev
TABLAS I · el único tipo compuesto

Constructores de tabla

Las tres formas del constructor de Lua: la lista posicional, el mapa con claves con nombre y las claves entre corchetes. Cómo se mezclan en un mismo literal, qué azúcar sintáctico esconde la clave con nombre y por qué la última expresión del constructor se comporta de forma distinta a todas las demás.

⏱ 14 min

El constructor de tabla es la construcción sintáctica más densa de Lua: en un solo par de llaves conviven tres notaciones distintas, una regla de numeración implícita, un azúcar sintáctico que oculta una conversión a cadena y una excepción sobre la última expresión que provoca más sorpresas que ninguna otra regla del lenguaje. Aprenderlo bien no es aprender a escribir literales bonitos, sino entender exactamente qué claves acaban existiendo, con qué valores y en qué orden se evalúan.

🎯 Al terminar esta lección sabrás
  • Distinguir la forma de lista, la forma de mapa y la forma con claves entre corchetes.
  • Entender qué azúcar sintáctico esconde la clave con nombre y cuáles son sus límites.
  • Mezclar las tres notaciones en un mismo literal sabiendo qué índice recibe cada elemento.
  • Dominar la regla de expansión de la última expresión y sus consecuencias prácticas.

Las tres notaciones

Un constructor es un par de llaves con cero o más campos separados por coma o por punto y coma. Cada campo puede escribirse de una de tres formas:

-- Forma de lista: solo el valor. Lua asigna 1, 2, 3... en orden
local a = { "rojo", "verde", "azul" }
-- equivale a: a[1]="rojo"; a[2]="verde"; a[3]="azul"

-- Forma de mapa: nombre = valor. La clave es la cadena con ese nombre
local b = { ancho = 80, alto = 24 }
-- equivale a: b["ancho"]=80; b["alto"]=24

-- Forma con corchetes: [expresion] = valor. La clave es cualquier valor
local k = "din" .. "amica"
local c = { [k] = 1, [10] = "diez", [true] = "si", [1.5] = "medio" }

La forma de mapa es estrictamente menos general que la de corchetes: nombre = v es azúcar de ["nombre"] = v. La consecuencia es que solo admite claves que sean identificadores válidos de Lua. Una clave con espacios, con guiones, que empiece por dígito o que coincida con una palabra reservada obliga a usar corchetes.

⚠️
La forma con nombre no evalúa el nombre

En { x = 1 } la clave es literalmente la cadena "x", no el valor de una variable llamada x. Si lo que quieres es usar el contenido de una variable como clave, la única forma correcta es la de corchetes: { [x] = 1 }. Este es el error más frecuente al construir tablas dinámicas, y es silencioso: no produce ningún fallo, solo una tabla con la clave equivocada.

flowchart TD
C[Campo del constructor] --> L[Solo valor]
C --> N[nombre igual valor]
C --> B[Corchetes con expresion]
L --> L1[Recibe el siguiente indice entero implicito]
N --> N1[Azucar de la clave de cadena]
B --> B1[Cualquier valor salvo nil y NaN]

Mezclar notaciones en un mismo literal

Nada impide combinar las tres formas. La regla que hay que interiorizar es que solo los campos de la forma de lista consumen el contador implícito; los otros dos lo ignoran por completo:

local t = {
  "a",              -- t[1] = "a"
  x = 10,           -- t["x"] = 10   (no toca el contador)
  "b",              -- t[2] = "b"
  [100] = "cien",   -- t[100] = "cien" (tampoco toca el contador)
  "c",              -- t[3] = "c"
}

De ahí se sigue un choque que conviene conocer: si un campo con corchetes escribe en un índice entero que el contador implícito también va a usar, gana el que se evalúe después, y el orden de evaluación de los campos es de arriba abajo.

local u = { [1] = "puesto a mano", "posicional" }
print(u[1])  --> posicional   (el campo de lista se evalua despues)
🗃️

Separadores intercambiables

La coma y el punto y coma son equivalentes como separadores. La convención habitual reserva el punto y coma para marcar visualmente la frontera entre la parte de lista y la parte de mapa.

🧾

Separador final permitido

Un separador sobrante al final del constructor es legal. Esto hace que los diagramas de cambios en control de versiones sean limpios al añadir o quitar la última línea.

🔁

Orden de evaluación

Los campos se evalúan en el orden en que aparecen. Si dos campos escriben la misma clave, el resultado final es el del último, sin aviso alguno del intérprete.

✂️

Azúcar de llamada

Cuando el único argumento de una función es un constructor, los paréntesis son opcionales. Esa regla es la base de las configuraciones declarativas escritas en Lua.

Esa última regla merece verse, porque explica el aspecto de casi todos los archivos de configuración escritos en Lua, incluidos los de los gestores de complementos de editores modernos:

local function ventana(opts) return opts.ancho * opts.alto end
print(ventana{ ancho = 80, alto = 24 })   -- sin parentesis: azucar de llamada

Los constructores anidan sin límite, y ese anidamiento es lo que convierte un literal en un árbol de datos completo. Como los valores pueden ser funciones, el árbol puede llevar comportamiento dentro, algo que ningún formato de datos puro permite:

local plugin = {
  nombre = "buscador",
  dependencias = { "plenary", "telescope" },
  opciones = {
    ventana = { borde = "redondeado", ancho = 0.8 },
    atajos  = {
      { modo = "n", tecla = "ff", accion = "buscar_archivos" },
      { modo = "n", tecla = "fg", accion = "buscar_texto" },
    },
  },
  al_cargar = function(self) return "cargado " .. self.nombre end,
}

Obsérvese que el campo atajos es una secuencia de registros: la forma de lista en el nivel exterior y la de mapa en el interior. Esa combinación —lista de tablas con campos con nombre— es, con diferencia, la estructura más frecuente en la configuración de programas reales escritos en Lua.

La regla de la última expresión

Aquí está la sutileza que más sorprende. Cuando una llamada a función o el operador de puntos suspensivos aparece como último campo de la forma de lista, se expande a todos sus valores de retorno. En cualquier otra posición, se recorta a uno solo:

local function tres() return "a", "b", "c" end

local x = { tres() }          -- #x == 3   : expande
local y = { tres(), "z" }     -- #y == 2   : recorta a "a", luego "z"
local z = { (tres()) }        -- #z == 1   : los parentesis fuerzan un valor

La misma regla vale para el parámetro variádico dentro de una función, y de ahí nace uno de los idiomas más usados del lenguaje: empaquetar los argumentos recibidos en una tabla.

local function registrar(...)
  local args = { ... }        -- ultima expresion: expande todo
  return #args
end
📝
Empaquetar con nil por medio

El idioma anterior pierde información si alguno de los argumentos es nil, porque el operador de longitud deja de ser fiable en cuanto hay agujeros. Cuando el recuento importa, la forma correcta es table.pack, que devuelve una tabla con un campo n que guarda el número real de argumentos recibidos, agujeros incluidos.

El constructor no es sintaxis: es la superficie declarativa de todo el lenguaje

Es tentador leer el constructor como una comodidad para no escribir tres asignaciones seguidas, pero su papel en el ecosistema es mucho mayor. Un constructor es la única construcción de Lua que permite describir una estructura de datos completa como un valor único y anidable, y esa propiedad es la que ha convertido a Lua en el formato de configuración de facto de una generación entera de programas. Cuando escribes la configuración de un editor, la definición de un nivel de videojuego o las reglas de un servidor web en Lua, no estás usando un lenguaje de datos con sintaxis parecida a un lenguaje de programación: estás ejecutando código real cuyo resultado es una tabla, y por eso puedes interpolar variables, llamar funciones, condicionar campos y componer fragmentos con una libertad que ningún formato puramente declarativo ofrece. El precio de esa libertad es exactamente lo que este apartado ha ido enumerando: el constructor tiene reglas de evaluación —el contador implícito que solo avanzan los campos posicionales, el orden estricto de arriba abajo, la última expresión que se expande y en cualquier otra posición se recorta— y esas reglas dejan de ser trivia en cuanto tu literal deja de ser una lista de constantes. Quien las ignora escribe configuraciones que funcionan por casualidad y fallan al reordenar dos líneas; quien las domina descubre que el constructor es un pequeño lenguaje declarativo empotrado dentro de uno imperativo, con la potencia del anfitrión y la legibilidad de un formato de datos. Esa dualidad, y no la brevedad, es la razón por la que las llaves de Lua aparecen en tantos programas que nunca se propusieron ser programas.

⚔️ Interrogar al constructor
  1. Escribe un literal que mezcle las tres notaciones y predice, antes de ejecutarlo, qué claves existirán y con qué valores; verifícalo recorriendo la tabla con pairs.
  2. Construye una tabla cuya clave provenga de una variable y comprueba la diferencia entre la forma con nombre y la forma con corchetes.
  3. Define una función que devuelva tres valores y compárala como único campo, como primer campo de dos y encerrada entre paréntesis.
  4. Escribe una función variádica que reciba nil en medio de sus argumentos y comprueba por qué table.pack es más fiable que el empaquetado directo.