Lua en 20 minutos: tipos, locales y funciones
El núcleo del lenguaje que Neovim ejecuta de verdad: LuaJIT y su dialecto 5.1, los ocho tipos y su semántica de valor o referencia, el alcance léxico de local y sus upvalues, funciones con retornos múltiples y la regla de ajuste, y las sorpresas que descolocan a quien viene de C, Python o JavaScript.
Aquí empieza la segunda mitad del camino: dejas de usar Neovim y empiezas a extenderlo. La puerta de entrada es Lua, un lenguaje diminuto —ocho tipos, una sola estructura de datos compuesta, veintiuna palabras reservadas— que puedes leer entero en una tarde. Esa pequeñez no es pobreza: es exactamente la razón por la que cabe dentro de un editor, arranca en milisegundos y no te obliga a aprender un ecosistema para cambiar un atajo.
- Saber qué dialecto exacto ejecuta Neovim (LuaJIT ≈ 5.1) y qué no debes copiar de tutoriales de 5.3+.
- Enumerar los ocho tipos de Lua y distinguir cuáles tienen semántica de valor y cuáles de referencia.
- Dominar
local, el alcance léxico y por qué una variable global es casi siempre un error. - Escribir funciones con retornos múltiples y predecir el ajuste de listas de valores.
- Reconocer las trampas clásicas: índices desde 1,
nilque borra y una noción de verdad muy estricta.
Qué Lua estás escribiendo exactamente
Neovim no embebe el intérprete de referencia: embebe LuaJIT, un compilador JIT compatible con Lua 5.1 más algunas extensiones tomadas de 5.2. Ese detalle no es trivia, porque internet está llena de tutoriales escritos para Lua 5.3 y 5.4 con sintaxis que aquí ni siquiera compila. Compruébalo en tu editor con :lua =jit.version y :lua =_VERSION: verás LuaJIT 2.1 y Lua 5.1.
| Verás en tutoriales de 5.3+ | En el Lua de Neovim |
|---|---|
a // b (división entera) |
Error de sintaxis. Se escribe math.floor(a / b) |
| Tipo entero de 64 bits separado del real | Un solo tipo number: doble IEEE 754 |
table.unpack |
No existe: la función es la global unpack |
| Operadores bit a bit como sintaxis | La biblioteca bit: bit.band, bit.bor, bit.lshift |
goto y etiquetas (nacieron en 5.2) |
Sí existen: LuaJIT los adoptó como extensión |
xpcall(f, handler, args...) (5.2) |
Sí funciona: otra extensión que LuaJIT incorporó |
La regla operativa: cuando un fragmento de internet no funcione, sospecha primero de la versión. Y cuando dudes, la referencia correcta es el manual de Lua 5.1 y :h lua-concepts, no el manual más nuevo que encuentre el buscador.
Que number sea siempre un doble tiene dos consecuencias prácticas. La primera: los enteros son exactos hasta 2^53, de sobra para números de línea, ids de buffer o marcas de tiempo en milisegundos; no vas a perder precisión donde importa. La segunda: 10 / 3 da 3.3333, no 3, y 0.1 + 0.2 == 0.3 es falso, como en todo lenguaje con coma flotante binaria. La división entera se escribe math.floor(a / b), y las comparaciones con decimales se hacen con tolerancia, no con ==.
Ocho tipos y ni uno más
print(type(nil)) --> nil ausencia de valorprint(type(true)) --> booleanprint(type(42)) --> number doble IEEE 754, no hay intprint(type("hola")) --> string inmutable e internadaprint(type({})) --> table la unica estructura compuestaprint(type(print)) --> function valor de primera claseprint(type(coroutine.create(function() end))) --> threadprint(type(vim.uv.new_timer())) --> userdataLos cuatro primeros son valores: se copian al asignarse y se comparan por contenido. Los cuatro últimos son referencias gestionadas por el recolector de basura: se copian por puntero y == compara identidad, no forma. De ahí que dos tablas con el mismo contenido nunca sean iguales.
local a, b = "opt", "opt"print(a == b) --> true las cadenas se internanlocal t1, t2 = { 1, 2 }, { 1, 2 }print(t1 == t2) --> false dos objetos distintosprint(t1 == t1) --> true identidadprint(vim.deep_equal(t1, t2)) --> true comparar por contenido es cosa tuyaSemántica de valor
nil, boolean, number, string. Asignar copia; == compara contenido. Una cadena que pasas a una función no puede ser modificada por ella: es inmutable.
Semántica de referencia
table, function, thread, userdata. Asignar copia el puntero; == compara identidad. Una tabla que pasas a una función sí puede ser mutada por ella, y lo notarás fuera.
Ese reparto explica media API de Neovim antes de haberla visto. Cuando la lección 7.4 te diga que require devuelve la misma tabla a todos los que la piden, o cuando en la 7.2 veas que vim.deepcopy existe porque asignar una tabla no la copia, no habrá nada nuevo: es la semántica de referencia haciendo su trabajo. Y userdata es la ventana al mundo exterior: los temporizadores y procesos de vim.uv que usarás en el Nivel 12 son objetos de C envueltos, opacos para Lua salvo por los métodos que exponen.
local no es un adorno opcional
En Lua toda variable es global por defecto. Omitir local no crea una variable de ámbito reducido: crea una entrada en la tabla _G, visible desde cualquier plugin y desde cualquier :lua posterior, viva hasta que cierres Neovim.
contador = 0 -- global: entra en _G, colisiona con todolocal contador = 0 -- local: vive en el bloque, la ve el compiladorLa diferencia también es de rendimiento. LuaJIT asigna cada local a un registro de su máquina virtual, mientras que cada lectura de una global es una búsqueda con hash en _G. En un callback que se dispara en cada pulsación, esa distancia se nota.
El alcance es léxico y por bloque: una local vive desde la línea siguiente a su declaración hasta el end (o el final del archivo) que la contiene. Los bloques anidan y las declaraciones interiores sombrean a las exteriores sin tocarlas:
local x = 1do -- un bloque explicito, util para acotar vidas local x = 2 -- sombrea a la de fuera print(x) --> 2endprint(x) --> 1 intactaUna función definida dentro de un bloque captura las locales que ve; a esas variables capturadas se les llama upvalues, y son la base de todo lo que verás en la lección 7.3. Por ahora basta el patrón que estructura cualquier archivo de configuración decente: locales privadas arriba, tabla pública abajo.
local M = {}local function ayuda() -- privada del archivo, no escapa return "solo visible aqui"endfunction M.publica() -- expuesta por la tabla M return ayuda()endreturn Mlocal function no es azúcar de local f = function
local function f() ... end declara la local antes de compilar el cuerpo, así que la función puede llamarse a sí misma. local f = function() ... f() ... end no: dentro del cuerpo, f todavía es una global inexistente y la recursión revienta con attempt to call a nil value. Para recursión, siempre la primera forma.
Las tres sorpresas que más cuestan
Uno: los índices empiezan en 1. No es un capricho; es coherente con toda la biblioteca estándar: lista[1] es el primer elemento, string.sub(s, 1, 3) son los tres primeros bytes, select(1, ...) es el primer argumento. La excepción que te morderá es la API de Neovim: nvim_buf_get_lines y compañía usan índices desde 0, porque hablan el protocolo de la API —pensado para clientes en cualquier lenguaje—, no el convenio de Lua. Desde el Nivel 8 convivirás con los dos convenios en el mismo archivo, y saber cuál usa cada capa es media batalla.
Dos: nil es ausencia, no un valor almacenable. Asignar nil a una clave la borra de la tabla. No hay diferencia observable entre una clave con valor nil y una clave que nunca existió, y esa ambigüedad se propaga a los argumentos de una función y a las listas con huecos que verás en la 7.2.
local t = { a = 1 }t.a = nilprint(next(t)) --> nil la tabla esta vacia, no tiene una clave con nilTres: solo nil y false son falsos. Cualquier otra cosa es verdadera, incluidos 0 y la cadena vacía. Quien viene de C o Python tropieza aquí una vez y solo una.
if 0 then print("si") end --> imprime: 0 es verdaderoif "" then print("si") end --> imprime: la cadena vacia es verdaderalocal n = tonumber(entrada) or 0 -- el idioma para valores por defectoEse or es el idioma más frecuente de todo el lenguaje: a or b devuelve a si es verdadero y b en caso contrario, y a and b devuelve b si a es verdadero. Fíjate en que devuelven el operando, no un booleano: por eso sirven para elegir valores. Combinados dan un ternario, cond and x or y, correcto siempre que x no sea falso: si x es false o nil, el and falla y el or te entrega y aunque la condición fuera verdadera. Es la trampa clásica de ese idioma, y la salida honesta es un if de tres líneas.
Hay una cuarta sorpresa que muerde más tarde: las cadenas son secuencias de bytes inmutables, no de caracteres. El operador # cuenta bytes: #"añadir" es 7, no 6, porque la ñ ocupa dos bytes en UTF-8. Para contar caracteres visibles necesitarás vim.fn.strchars, que verás en la lección 7.5. Y como cada concatenación con .. crea una cadena nueva, construir texto acumulando en un bucle es cuadrático: lo correcto es acumular los trozos en una tabla y cerrar con table.concat, el patrón que verás en la 7.2.
print(#"añadir") --> 7 bytes, no 6 caracteresprint("linea " .. 42) --> coercion de numero a cadenaprint(1 == "1") --> false: la igualdad NO coercionaprint("10" + 5) --> 15: la aritmetica SI coercionaLa asimetría de las coerciones
La aritmética convierte cadenas a números y la concatenación convierte números a cadenas, pero == no convierte nada: con tipos distintos devuelve false sin protestar. Esa asimetría es deliberada y evita el pozo de comparaciones sorprendentes de otros lenguajes, a costa de que tonumber y tostring sean explícitos en tu código. Ojo con la precedencia: "n" .. 1 + 2 imprime n3, porque la suma liga más fuerte que la concatenación.
Funciones, retornos múltiples y ajuste
Las funciones son valores de primera clase: se guardan en variables, se pasan como argumentos, se devuelven y se meten en tablas. Y pueden devolver varios valores, algo que casi ningún lenguaje de su generación permitía.
local function dividir(a, b) if b == 0 then return nil, "division por cero" end return math.floor(a / b), a % bendlocal q, r = dividir(17, 5) --> q = 3, r = 2local ok, err = dividir(1, 0) --> ok = nil, err = "division por cero"Ese par valor, error es el convenio de fallo de Lua —el mismo que formaliza pcall en la lección 7.3— y el que usa media API de Neovim. La regla que gobierna cuántos valores sobreviven se llama ajuste: una llamada en la última posición de una lista de expresiones se expande a todos sus valores; en cualquier otra posición se trunca al primero. Rodearla de paréntesis también la trunca. La misma regla recorta o rellena con nil las asignaciones múltiples y los argumentos de una llamada.
local t = { dividir(17, 5) } --> { 3, 2 } ultima posicion: expandelocal u = { dividir(17, 5), 99 } --> { 3, 99 } posicion intermedia: truncalocal v = (dividir(17, 5)) --> 3 los parentesis truncanlocal x, y, z = dividir(17, 5) --> 3, 2, nil el ajuste rellenaEl azúcar que verás por todas partes
Cuando el único argumento de una llamada es una cadena literal o un constructor de tabla, los paréntesis son opcionales. No es una curiosidad: es la razón de que media configuración de Neovim tenga el aspecto que tiene.
require "mi.modulo" -- igual que require("mi.modulo")require("plugin").setup { ancho = 80 } -- setup({ ancho = 80 })vim.cmd [[colorscheme habamax]] -- una cadena larga entre corchetesLos corchetes dobles delimitan cadenas largas: multilínea, sin secuencias de escape, ideales para incrustar Vimscript o texto con barras invertidas. Cuando en la 7.5 veas v:lua.require'mi.modulo'.f() desde Vimscript, reconocerás este azúcar: es una llamada sin paréntesis con argumento de cadena.
La economía de un lenguaje que cabe en la cabeza
Merece la pena detenerse en lo que acabas de ver, porque explica por qué Neovim eligió Lua y no Python, Ruby o JavaScript. Todo el lenguaje son ocho tipos, una estructura compuesta, alcance léxico y funciones de primera clase; no hay clases, ni interfaces, ni módulos como constructo sintáctico, ni excepciones, ni herencia, ni decoradores. Lo que en otros lenguajes son mecanismos del núcleo, en Lua son patrones que construyes con lo poco que hay: un módulo es una tabla que devuelves, un objeto es una tabla con una metatabla, la herencia es una cadena de __index, y el manejo de errores es un valor devuelto más una función llamada pcall. Esa decisión de diseño tiene un coste —tendrás que escribir a mano lo que otros te dan hecho— y una recompensa enorme: el intérprete entero pesa unos cientos de kilobytes, arranca sin ceremonia, no impone un modelo de objetos que choque con el del anfitrión y puede detenerse y reanudarse dentro del bucle de eventos de un editor. Neovim no adoptó Lua porque fuera bonito, sino porque un lenguaje incrustable debe ser pequeño, predecible y sin opiniones. Cuando en las próximas lecciones veas que las tablas hacen de array, de diccionario, de objeto, de módulo y de espacio de nombres a la vez, no lo leas como una carencia: es el mismo principio que hace que puedas configurar tu editor sin instalar un ecosistema.
Drills
Verás LuaJIT 2.1 y Lua 5.1. Es el recordatorio permanente de qué manual leer y de por qué a // b no compila aunque medio internet lo escriba.
false 15 linea 42. La igualdad no convierte, la aritmética y la concatenación sí. Tres reglas distintas para tres operadores.
next sobre una tabla vacía devuelve nil. No existe una tabla con una clave cuyo valor sea nil: la asignación la eliminó.
Sin local, la variable entró en _G y cualquier :lua posterior —y cualquier plugin— la ve. Limpia con :lua fuga = nil.
Comprueba que el modelo mental encaja
- En tu Neovim, ejecuta
:lua =jit.versiony:lua =_VERSION, y confirma que:lua =7 // 2da error de sintaxis mientras:lua =math.floor(7 / 2)responde3. - Ejecuta
:lua local t1, t2 = {1, 2}, {1, 2}; print(t1 == t2, vim.deep_equal(t1, t2))y explica por qué las dos respuestas difieren. - Predice el resultado de
print(#{ dividir(17, 5), dividir(9, 2) })en un papel, definedividircon:luay comprueba tu predicción. - Escribe una función
clamp(v, lo, hi)que devuelva el valor acotado y un booleano indicando si hubo recorte, y pruébala con:lua print(clamp(15, 0, 10)). Después ejecuta:lua print((clamp(15, 0, 10)))y explica adónde fue el booleano. - Ejecuta
:lua local x = false; print(true and x or "defecto")y explica por qué imprimedefectocuando el ternario prometíax. - Ejecuta
:lua =#"añadir"y:lua =("añadir"):sub(1, 2), y explica los dos resultados en términos de bytes.