Saltar al contenido

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, nil que 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

LUA
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()))                --> userdata

Los 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.

LUA
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 tuya

Semá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.

LUA
contador = 0            -- global: entra en _G, colisiona con todolocal contador = 0      -- local: vive en el bloque, la ve el compilador

La 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:

LUA
local x = 1do                       -- un bloque explicito, util para acotar vidas  local x = 2            -- sombrea a la de fuera  print(x)               --> 2endprint(x)                 --> 1   intacta

Una 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.

LUA
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 M

local 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.

LUA
local t = { a = 1 }t.a = nilprint(next(t))          --> nil   la tabla esta vacia, no tiene una clave con nil

Tres: 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.

LUA
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 defecto

Ese 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.

LUA
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 coerciona

La 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.

LUA
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.

LUA
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 rellena

El 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.

LUA
require "mi.modulo"                 -- igual que require("mi.modulo")require("plugin").setup { ancho = 80 }  -- setup({ ancho = 80 })vim.cmd [[colorscheme habamax]]     -- una cadena larga entre corchetes

Los 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

  1. En tu Neovim, ejecuta :lua =jit.version y :lua =_VERSION, y confirma que :lua =7 // 2 da error de sintaxis mientras :lua =math.floor(7 / 2) responde 3.
  2. 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.
  3. Predice el resultado de print(#{ dividir(17, 5), dividir(9, 2) }) en un papel, define dividir con :lua y comprueba tu predicción.
  4. 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.
  5. Ejecuta :lua local x = false; print(true and x or "defecto") y explica por qué imprime defecto cuando el ternario prometía x.
  6. Ejecuta :lua =#"añadir" y :lua =("añadir"):sub(1, 2), y explica los dos resultados en términos de bytes.