Funciones y closures: valores, varargs y pcall
Funciones como valores de primera clase, upvalues compartidos entre closures, el manejo correcto de varargs y los nil intermedios, pcall y xpcall para que un error no tumbe el editor, y una primera mirada a las metatablas que sostienen vim.opt.
Una configuración de Neovim es, en el fondo, un montón de funciones esperando a que pase algo: un autocmd que reacciona a un evento, un keymap con callback, un handler del LSP, un temporizador de libuv. Todas se ejecutan dentro del proceso del editor, así que una función mal escrita no lanza una excepción bonita: te deja el editor a medio configurar. Esta lección va de escribir funciones que capturan lo que deben y fallan como es debido.
- Explicar qué es un upvalue y cómo dos closures comparten estado.
- Manejar varargs contando argumentos con
selecten vez de con el operador de longitud. - Blindar código frágil con
pcallyxpcall, distinguiendo error de valor de retorno. - Leer una metatabla y reconocer los mecanismos que hay bajo
vim.optyvim.fn.
Closures: funciones que recuerdan
Una función anónima que referencia una variable local del ámbito exterior no copia su valor: captura la variable misma. Esa variable capturada se llama upvalue, y sobrevive al bloque que la creó porque el recolector de basura la mantiene viva mientras la función exista.
local function crear_contador()
local n = 0 -- upvalue capturado abajo
return function()
n = n + 1
return n
end
end
local a, b = crear_contador(), crear_contador()
print(a(), a(), b()) --> 1 2 1 estados independientes
Dos closures creadas en el mismo ámbito comparten el mismo upvalue: si una fábrica devuelve dos funciones que leen y escriben la misma local, ambas operan sobre el mismo estado. Ese es el patrón con el que se construye encapsulación sin objetos ni clases.
El caso que aparece en toda configuración real es el de la fábrica de callbacks: en lugar de escribir treinta funciones casi idénticas, escribes una que las genera.
local function mover_ventana(dir)
return function() vim.cmd.wincmd(dir) end
end
for _, dir in ipairs({ "h", "j", "k", "l" }) do
vim.keymap.set("n", "<C-" .. dir .. ">", mover_ventana(dir), { desc = "Ir a " .. dir })
end
En Lua cada iteración de un for crea una variable nueva para el control del bucle, así que capturarla en un closure es seguro. Es el comportamiento que JavaScript solo consiguió al pasar de var a let. Ojo: eso vale para la variable del for, no para una local declarada fuera del bucle.
Varargs: contar bien los argumentos
Los tres puntos declaran un número variable de argumentos. El error clásico es empaquetarlos con un constructor y medirlos con el operador de longitud: si alguno es nil, el conteo miente. La forma correcta es select("#", ...), que devuelve el número real de argumentos recibidos, nil incluidos.
local function registrar(nivel, ...)
local partes = {}
for i = 1, select("#", ...) do -- cuenta de verdad
partes[i] = vim.inspect((select(i, ...))) -- i-esimo argumento
end
vim.notify(table.concat(partes, " "), nivel)
end
registrar(vim.log.levels.WARN, "ruta", nil, 42) -- tres argumentos, no uno
El otro uso constante es el reenvío transparente: una envoltura que recibe cualquier firma y la pasa intacta.
local unpack = table.unpack or unpack -- LuaJIT expone la global unpack
local function cronometrar(fn, nombre)
return function(...)
local t0 = vim.uv.hrtime()
local res = { fn(...) } -- captura TODOS los retornos
vim.notify(("%s: %.2f ms"):format(nombre, (vim.uv.hrtime() - t0) / 1e6))
return unpack(res)
end
end
pcall: que un error no tumbe el editor
Lua no tiene excepciones con tipos ni bloques try. Tiene error, que aborta la ejecución, y pcall, que ejecuta una función en modo protegido y devuelve un booleano seguido del resultado o del mensaje de error.
local ok, mod = pcall(require, "telescope")
if not ok then
vim.notify("telescope no disponible: " .. mod, vim.log.levels.WARN)
return
end
mod.setup({})
Ese idioma es el más repetido de todo el ecosistema, y con razón: si un plugin falta o su setup revienta, el resto de tu configuración sigue cargándose. Sin el pcall, el primer error corta el archivo entero y arrancas con medio editor configurado y sin saber por qué.
El valor que se lanza no tiene por qué ser una cadena: error acepta cualquier valor, incluida una tabla, lo que permite errores estructurados. El segundo parámetro es el nivel, que decide a qué posición del código se le echa la culpa: 1 a la función que llama a error, 2 a quien la llamó, 0 para no añadir posición ninguna.
local function abrir(ruta)
if type(ruta) ~= "string" then
error({ code = "EARG", msg = "ruta debe ser string" }, 2) -- culpa al llamante
end
end
local ok, e = pcall(abrir, 42)
print(ok, type(e), e.code) --> false table EARG
Cuando además quieres la traza, xpcall acepta un manejador que se ejecuta antes de desenrollar la pila, que es el único momento en que la traza aún existe.
local ok, err = xpcall(arriesgado, debug.traceback, arg1, arg2)
if not ok then vim.schedule(function() vim.notify(err, vim.log.levels.ERROR) end) end
flowchart TD
A[Llamada protegida] --> B{Se lanza un error}
B -- No --> C[Devuelve true y los resultados]
B -- Si --> D[Devuelve false y el valor del error]
C --> F[El editor continua configurandose]
D --> F
style C fill:#a6e3a1,color:#11111b
style D fill:#f38ba8,color:#11111bEnvolver todo en pcall y descartar el mensaje convierte un fallo ruidoso en uno silencioso, que es mucho peor de depurar. Protege lo que puede fallar de forma esperable —cargar un módulo opcional, hablar con un proceso externo— y notifica siempre el error que capturas.
Metatablas: el gancho bajo vim.opt
Una metatabla es una tabla asociada a otra que define qué ocurre en operaciones que, de otro modo, fallarían o serían triviales. Los tres ganchos que necesitas conocer son __index, que se consulta al leer una clave ausente; __newindex, que intercepta la escritura de una clave nueva; y __call, que hace que una tabla se pueda invocar como función.
local perezoso = setmetatable({}, {
__index = function(t, k) -- solo se llama si k NO existe
local v = cargar_caro(k)
rawset(t, k, v) -- cachea sin redisparar el gancho
return v
end,
}) -- perezoso.lsp carga bajo demanda
Esto no es teoría: es literalmente cómo funciona la superficie de Neovim que ya usas. vim.fn no contiene las cientos de funciones de Vimscript, sino un __index que las resuelve al vuelo; vim.g, vim.b y vim.o traducen lectura y escritura a llamadas de la API mediante __index y __newindex; y vim.opt devuelve un objeto con metatabla cuyos métodos append, remove y get operan sobre opciones de lista. Cuando escribes vim.opt.clipboard:append("unnamedplus"), estás invocando un método sobre una tabla que se fabricó al instante.
La otra cara es la orientación a objetos idiomática, que no es más que __index apuntando a una tabla de métodos y el azúcar de los dos puntos, que inserta self como primer argumento.
local Cola = {}
Cola.__index = Cola -- los metodos se buscan aqui
function Cola.new() return setmetatable({ n = 0 }, Cola) end
function Cola:push(v) self.n = self.n + 1; self[self.n] = v; return self end
local q = Cola.new():push("a"):push("b") -- encadenable por el return self
Junta las tres piezas de esta lección y verás el mismo principio en cada una. Una closure no es una construcción del compilador: es una función más una lista de upvalues, y con eso solo ya tienes estado privado, memoización, inyección de dependencias y fábricas de callbacks sin que el lenguaje añada una sola palabra clave. El manejo de errores no es un sistema de excepciones con jerarquías: es error lanzando un valor cualquiera y pcall devolviendo un booleano, de modo que un fallo es un dato que decides propagar o absorber, y no un canal paralelo de control que se escapa de tu vista. Y las metatablas no son un sistema de objetos: son ganchos sobre operaciones primitivas que te dejan redefinir el significado de indexar, escribir y llamar, y sobre esa base mínima se levantan la herencia, los proxies perezosos y la API entera de vim. El patrón es siempre el mismo: en vez de darte una función del lenguaje, Lua te da el mecanismo con el que esa función se construye. Por eso al leer el código de un plugin no encuentras magia, sino tablas, funciones y setmetatable; y por eso, cuando algo se comporte de forma inesperada, siempre podrás abrir la tabla, mirar su metatabla y ver el gancho exacto que lo explica. Aprende el mecanismo y dejarás de memorizar API.
- Implementa una función
memo(fn)que cachee resultados por argumento usando un upvalue, y explica cuándo esa caché se libera. - Escribe una envoltura que registre los argumentos de cualquier función respetando los
nilintermedios, y demuestra por qué el operador de longitud fallaría. - Convierte tres
requirede tu configuración al idiomapcally notifica el error capturado con el nivel adecuado. - Lanza un error con una tabla y nivel
2, captúralo y explica qué posición del código aparece en el mensaje. - Construye un proxy con
__indexy__newindexque registre cada lectura y escritura de una tabla de opciones, y relaciónalo con lo que hacevim.g.