Tablas: array y diccionario a la vez
La única estructura compuesta de Lua por dentro: la parte array y la parte hash de LuaJIT, el operador de longitud y su borde indefinido, pairs frente a ipairs, y el arsenal vim.tbl_ de Neovim con vim.iter como sucesor moderno.
Lua tiene exactamente una estructura de datos compuesta, y con ella construye todo lo demás: listas, diccionarios, objetos, módulos, espacios de nombres y hasta el propio entorno global. Entender cómo está hecha una tabla por dentro —y dónde deja de comportarse como esperas— es la diferencia entre una configuración que funciona y una que funciona siempre.
- Describir la parte array y la parte hash de una tabla en LuaJIT y su efecto en el rendimiento.
- Explicar por qué el operador de longitud es indefinido cuando hay huecos.
- Elegir con criterio entre
pairs,ipairsyvim.iterpara recorrer datos. - Usar
vim.tbl_deep_extendy compañía sabiendo exactamente qué hacen con las listas.
Una tabla, dos almacenes
Una tabla es un mapa de claves a valores donde la clave puede ser cualquier valor salvo nil y NaN. Pero por dentro, LuaJIT la parte en dos zonas: un vector contiguo para las claves enteras que van de 1 hacia arriba, y una tabla hash para todo lo demás. El constructor decide en cuál cae cada elemento.
local lista = { "uno", "dos", "tres" } -- parte array: 1, 2, 3
local dicc = { nombre = "nvim", version = 12 } -- parte hash
local mixta = { "uno", nombre = "nvim" } -- las dos a la vez
print(lista[1]) --> uno indice 1, no 0
print(dicc.nombre) --> nvim azucar de dicc["nombre"]
print(dicc["ver" .. "sion"]) --> 12 la clave se calcula
La parte array es memoria contigua sin claves almacenadas: indexar es aritmética de punteros y recorrerla es amistoso con la caché. La parte hash guarda pares clave-valor y resuelve colisiones con encadenamiento interno. Por eso una lista de mil elementos con claves 1..1000 es órdenes de magnitud más rápida que la misma información con claves "1".."1000".
flowchart LR T[Una tabla de Lua] --> A[Parte array] T --> H[Parte hash] A --> A1[Claves enteras densas desde 1] A --> A2[Memoria contigua y sin claves] H --> H1[Cadenas booleanos tablas funciones] H --> H2[Enteros dispersos o negativos] style A fill:#a6e3a1,color:#11111b style H fill:#cba6f7,color:#11111b
Cuando la tabla crece, LuaJIT rehashea y redimensiona ambas zonas eligiendo el tamaño de array que deje al menos la mitad de las ranuras ocupadas. Insertar mil elementos uno a uno provoca varios rehasheos; construir la tabla de golpe con un constructor, ninguno. En bucles calientes, esa diferencia es medible.
El operador de longitud y su borde
#t no cuenta elementos: devuelve un borde, es decir, un índice n tal que t[n] no es nil y t[n+1] sí lo es. Si la secuencia tiene huecos, hay varios bordes válidos y el lenguaje puede devolver cualquiera de ellos. No es un fallo de implementación: la especificación lo declara indefinido.
local t = { 1, 2, 3 }
t[5] = 5
print(#t) --> 3 o 5, ambos son respuestas legitimas
De ahí la regla operativa: una lista no debe tener huecos. Para borrar del centro usa table.remove, que desplaza el resto; nunca asignes nil a mitad de una lista que vayas a recorrer con ipairs o medir con #.
table.insert(lista, "cuatro") -- al final
table.insert(lista, 1, "cero") -- en posicion, desplaza
table.remove(lista, 2) -- borra y compacta
print(table.concat(lista, ", ")) -- une con separador
table.sort(lista, function(a, b) return #a < #b end)
Los dos iteradores no son intercambiables. ipairs recorre 1, 2, 3, ... y se detiene en el primer nil; ignora por completo la parte hash. pairs recorre todas las claves, pero sin orden garantizado: el orden depende del hash interno y puede cambiar entre ejecuciones.
Si iteras con pairs para construir una cadena de configuración o una lista de plugins, el resultado puede variar entre arranques. Cuando el orden importe, recoge las claves con vim.tbl_keys, ordénalas con table.sort y recorre esa lista.
El arsenal vim.tbl_ y el relevo de vim.iter
Neovim añade una capa de utilidades porque la biblioteca estándar de 5.1 es deliberadamente escueta. Estas son las que aparecen a diario en cualquier configuración seria:
Consultar
vim.tbl_isempty, vim.tbl_count, vim.tbl_contains, vim.tbl_keys, vim.tbl_values y vim.tbl_get para leer rutas anidadas sin encadenar comprobaciones.
Transformar
vim.tbl_map y vim.tbl_filter sobre listas, vim.deepcopy para clonar de verdad, y vim.list_extend o vim.list_slice para trabajar por rangos.
Fusionar
vim.tbl_extend y vim.tbl_deep_extend, el mecanismo con el que toda distribución mezcla tus opciones con las suyas.
local cfg = { ui = { border = "rounded" }, hosts = { "a", "b" } }
print(vim.tbl_get(cfg, "ui", "border")) --> rounded, y nil si falta un tramo
print(vim.tbl_count(cfg)) --> 2, cuenta claves de cualquier tipo
print(vim.tbl_contains(cfg.hosts, "b")) --> true
local largos = vim.tbl_map(function(h) return h .. ".local" end, cfg.hosts)
local copia = vim.deepcopy(cfg) -- copia recursiva, no comparte tablas
La distinción entre lista y diccionario, tan borrosa en Lua, se consulta con vim.islist —comprueba claves consecutivas desde 1— y con vim.isarray, que tolera huecos. Ambas sustituyeron a las antiguas vim.tbl_islist y vim.tbl_isarray, igual que vim.tbl_flatten cedió el puesto a vim.iter.
vim.iter es la apuesta moderna: envuelve una tabla o una función iteradora y expone un objeto encadenable y perezoso, donde nada se materializa hasta que llamas a totable o fold.
local nombres = vim.iter(vim.api.nvim_list_bufs())
:filter(function(b) return vim.bo[b].buflisted end)
:map(function(b) return vim.api.nvim_buf_get_name(b) end)
:filter(function(n) return n ~= "" end)
:totable()
local total = vim.iter({ { 1, 2 }, { 3 } }):flatten():fold(0, function(acc, n)
return acc + n
end)
Fusionar configuraciones sin sorpresas
vim.tbl_extend(comportamiento, ...) mezcla tablas a un solo nivel; vim.tbl_deep_extend desciende recursivamente. El primer argumento decide qué pasa ante una clave repetida: "force" deja ganar a la última tabla, "keep" a la primera y "error" aborta.
local defecto = { ui = { border = "single", ancho = 80 }, srv = { "lua_ls" } }
local usuario = { ui = { border = "rounded" }, srv = { "clangd" } }
local plano = vim.tbl_extend("force", defecto, usuario)
-- plano.ui = { border = "rounded" } se perdio ancho: solo un nivel
local hondo = vim.tbl_deep_extend("force", defecto, usuario)
-- hondo.ui = { border = "rounded", ancho = 80 } se fusiono
-- hondo.srv = { "clangd" } la LISTA se reemplaza entera
Esa última línea es el detalle que arruina tardes enteras: la fusión profunda recorre diccionarios, pero al encontrar dos listas no las concatena, las sustituye. Si quieres añadir a una lista existente, vim.list_extend es la herramienta correcta, y ese es exactamente el motivo por el que los gestores de plugins ofrecen la forma de función opts = function(_, opts) ... end en lugar de una tabla plana.
Detente en la magnitud de la decisión: Lua no tiene listas, ni tuplas, ni diccionarios, ni conjuntos, ni registros, ni clases. Tiene tablas. Un módulo es una tabla que devuelves; un objeto es una tabla con metatabla; un espacio de nombres es una tabla anidada; el entorno global es la tabla _G; vim.opt, vim.bo y vim.g son tablas con comportamiento inyectado. El coste de esa unificación lo has visto en esta lección: el operador de longitud tiene un borde indefinido, pairs no garantiza orden, y distinguir una lista de un diccionario exige una heurística en vez de un tipo. Pero mira lo que compras a cambio. Una única implementación bien optimizada —el par array más hash— cubre todos los casos de uso, así que el motor entero es minúsculo y su rendimiento, uniforme y predecible. Una única API que aprendes una vez y aplicas en todas partes: next, la indexación y las metatablas bastan para razonar sobre cualquier dato del sistema. Y una única representación al cruzar la frontera hacia la API de Neovim, donde una tabla se convierte en lista o en diccionario de MessagePack según su forma. Cuando escribas require("plugin").setup({ ... }) recuerda que esa llave abre el mismo objeto que sostiene el lenguaje entero: si dominas la tabla, dominas Lua, porque en Lua no hay nada más que dominar.
- Construye una tabla con un hueco deliberado y explica por qué
#tpuede devolver dos valores distintos igual de correctos. - Escribe una función que reciba un diccionario y devuelva sus claves ordenadas alfabéticamente, sin depender del orden de
pairs. - Reescribe un bucle
forde tu configuración con una cadena devim.itery razona qué gana en claridad y qué en trabajo diferido. - Demuestra con un ejemplo mínimo que
vim.tbl_deep_extendreemplaza listas en vez de concatenarlas, y arréglalo convim.list_extend. - Justifica por qué
vim.deepcopyes imprescindible antes de mutar una tabla de opciones que te ha entregado un plugin.