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, la caja de herramientas de table, 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.
- Manejar la caja de herramientas de
table:insert,remove,sortyconcat, con sus trampas. - 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, 3local dicc = { nombre = "nvim", version = 12 } -- parte hashlocal mixta = { "uno", nombre = "nvim" } -- las dos a la vezprint(lista[1]) --> uno indice 1, no 0print(dicc.nombre) --> nvim azucar de dicc["nombre"]print(dicc["ver" .. "sion"]) --> 12 la clave se calculaLa notación con punto y la notación con corchetes son la misma operación: t.clave es azúcar de t["clave"], válido cuando la clave es un identificador. Los corchetes hacen falta cuando la clave se calcula, contiene caracteres raros o no es una cadena: t[1], t[otra_tabla], t["con espacios"].
Parte array
Claves enteras densas desde 1. Memoria contigua sin claves almacenadas: indexar es aritmética de punteros y recorrerla es amistoso con la caché del procesador.
Parte hash
Cadenas, booleanos, tablas, funciones, y también los enteros dispersos, negativos o el cero. Guarda pares clave-valor y resuelve colisiones encadenando dentro de la propia tabla.
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": la primera vive en el vector, la segunda paga un hash por acceso. Cuando la tabla crece, LuaJIT rehashea: 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 de 5.1 lo declara indefinido, y el resultado concreto depende de en qué zona interna cayó cada clave.
local t = { 1, 2, 3 }t[5] = 5print(#t) --> 3 o 5: ambos son bordes legitimosDe 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 #. Y si necesitas marcar «aquí no hay valor» sin romper la secuencia, guarda un centinela explícito —false, o la propia vim.NIL que conocerás en la 7.5— en vez de un agujero.
La caja de herramientas de table
La biblioteca table de 5.1 es corta y la usarás entera:
table.insert(lista, "cuatro") -- al final: lista[#lista+1] = v es equivalentetable.insert(lista, 1, "cero") -- en posicion, desplaza lo demastable.remove(lista) -- quita y devuelve el ultimotable.remove(lista, 2) -- borra en posicion y compactatable.sort(lista) -- en el sitio, con el orden de <table.sort(lista, function(a, b) return #a < #b end) -- o con comparadorprint(table.concat(lista, ", ")) -- une una lista de cadenas o numerosDos avisos que ahorran tardes. table.sort ordena en el sitio —muta la tabla, no devuelve una copia— y su comparador debe ser un orden estricto: significa «a va antes que b», y con elementos iguales debe devolver false. Un comparador con <= puede reventar con invalid order function for sorting en cuanto la lista crezca. Y table.concat es la otra mitad del consejo de la lección 7.1: como cada .. crea una cadena nueva, un bucle que concatena acumula copias cuadráticas; acumular los trozos en una lista y unirlos al final con concat es lineal.
local partes = {}for i = 1, 1000 do partes[#partes + 1] = "linea " .. i -- barato: mil cadenas cortasendlocal doc = table.concat(partes, "\n") -- una sola union al finalpairs e ipairs 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 —array y hash—, pero sin orden garantizado: el orden depende del estado interno del hash y puede cambiar entre ejecuciones. Con la lista agujereada de antes, los tres mecanismos responden cosas distintas:
local h = { "a", "b", nil, "d" }-- ipairs: recorre 2 elementos (para en el hueco)-- pairs: visita 3 claves (1, 2 y 4, en algun orden)-- #h: puede decir 2 o 4Nunca dependas del orden de pairs
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. Es el idioma estándar para «diccionario en orden alfabético», y lo usarás en cualquier cosa que imprima configuración.
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 tramoprint(vim.tbl_count(cfg)) --> 2, cuenta claves de cualquier tipoprint(vim.tbl_contains(cfg.hosts, "b")) --> truelocal largos = vim.tbl_map(function(h) return h .. ".local" end, cfg.hosts)local copia = vim.deepcopy(cfg) -- copia recursiva, no comparte tablasvim.deepcopy merece una frase más, porque su necesidad viene directa de la lección 7.1: asignar una tabla copia la referencia, así que «guardar» una tabla de opciones antes de mutarla no guarda nada. La copia profunda crea tablas nuevas en cada nivel, y es lo que quieres antes de tocar una tabla que te entregó un plugin —porque él conserva su referencia y verá tus mutaciones—.
La distinción entre lista y diccionario, tan borrosa en Lua, se consulta con vim.islist —claves consecutivas desde 1— y con vim.isarray, que tolera enteros no contiguos. Ambas sustituyeron a las antiguas vim.tbl_islist y vim.tbl_isarray, igual que vim.tbl_flatten cedió el puesto a vim.iter. Verás las viejas en configuraciones antiguas: no las copies.
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 + nend)Sobre una lista, la cadena itera como ipairs; sobre un diccionario, como pairs —con su orden no garantizado—, y en ese caso los callbacks reciben clave y valor. La familia sigue creciendo: 0.12 añadió Iter:unique() para deduplicar y dejó que take y skip acepten predicados, además del módulo vim.list con utilidades como vim.list.unique. No hace falta memorizarla entera; hace falta saber que, cuando encadenes dos transformaciones sobre una colección, este es el idioma que la documentación moderna espera.
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. Desde 0.12 también acepta una función (clave, valor_previo, valor_nuevo) que decide caso a caso, para cuando ninguna de las tres políticas encaja.
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 nivellocal hondo = vim.tbl_deep_extend("force", defecto, usuario)-- hondo.ui = { border = "rounded", ancho = 80 } se fusiono-- hondo.srv = { "clangd" } la LISTA se reemplaza enteraEsa última línea es el detalle que arruina tardes enteras, y no es un descuido sino una decisión documentada: la fusión profunda recorre diccionarios, pero trata las listas como valores opacos y las sustituye enteras. Tiene sentido —si el usuario da su lista de servidores, casi nunca quiere los suyos más los de defecto—, pero significa que «añadir un elemento» no se hace fusionando. Para eso está vim.list_extend, que concatena en el sitio; y esa asimetría es exactamente el motivo por el que los gestores de plugins ofrecen una forma de función para modificar opciones, en vez de dejarte solo una tabla que se fusionará.
-- Quiero los servidores por defecto Y el mio:local cfg = vim.deepcopy(defecto)vim.list_extend(cfg.srv, { "clangd" }) -- cfg.srv = { "lua_ls", "clangd" }Una sola estructura, y por eso la conoces entera
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, como verás en la 7.5. 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.
Drills
Anota lo que devuelve tu Neovim. Lo importante no es el número concreto: es que la especificación permite el otro, y que ningún código tuyo debe depender de cuál sale.
true false true. islist exige claves consecutivas desde 1; isarray acepta enteros con huecos. La forma de la tabla es lo único que hay: no existe un tipo lista.
alfa, media, zeta, siempre. tbl_keys recoge, table.sort ordena, y recorres la lista ordenada en vez del diccionario. El idioma completo contra el orden aleatorio de pairs. Ojo: no funcionaría sobre vim.g, que no es una tabla de verdad sino un proxy con metatabla —lo verás en la 7.3—.
Una lista de la API, un map y la materialización final con totable. La versión con bucle son cinco líneas; la cadena cuenta la misma historia en una.
Pon a prueba la única estructura
- Construye con
:luauna tabla con un hueco deliberado, imprime#t, y explica por qué el valor que ves y el otro son igual de correctos. - Escribe una función que reciba un diccionario y devuelva sus claves ordenadas alfabéticamente, y pruébala desde
:luasobrepackage.loaded, que es un diccionario de verdad. - Demuestra en una sola línea de
:luaqueipairsse detiene en un hueco dondepairscontinúa, contando iteraciones de cada uno. - Reproduce el ejemplo de
defectoyusuariode esta lección, imprimehondo.srvconvim.printy comprueba que la lista fue reemplazada; arréglalo después convim.deepcopymásvim.list_extendy verifica que ahora hay dos servidores. - Demuestra por qué
vim.deepcopyimporta: guarda una tabla anidada en dos variables con=y muta una; luego repite condeepcopyy compara convim.deep_equalqué cambió en cada caso.