Cheatsheet Lua
Linguagem de scripting leve para jogos, embedded e automação
Lua
Básico e Tipos
Variables y tipos
-- Variables locales (recomendado):
local nombre = "Ana"
local edad = 30
local pi = 3.14159
local activo = true
local nada = nil
-- Variables globales (¡evitar!):
contador = 0 -- va a _G
-- Tipos (type() devuelve string):
type("hola") -- "string"
type(42) -- "number"
type(true) -- "boolean"
type(nil) -- "nil"
type({}) -- "table"
type(print) -- "function"Lua tiene tipado dinámico con 8 tipos: nil, boolean, number, string, table, function, userdata y thread. La keyword local es esencial — las variables sin ella son globales (almacenadas en _G), causando bugs y peor rendimiento.
Conversión de tipos
-- tostring (cualquier tipo -> string):
tostring(42) -- "42"
tostring(true) -- "true"
tostring(nil) -- "nil"
-- tonumber (string -> número):
tonumber("42") -- 42
tonumber("3.14") -- 3.14
tonumber("0xFF") -- 255 (¡hex!)
tonumber("abc") -- nil (¡falla!)
tonumber("42", 8) -- 34 (base 8)
-- Coerción automática:
"10" + 5 -- 15 (string -> number)
"10" .. 5 -- "105" (number -> string)
-- Verificar antes de usar:
local n = tonumber(input)
if n then
print("Número: " .. n)
endLua hace coerción automática en contextos aritméticos ("10" + 5 = 15) y de concatenación. tonumber() devuelve nil cuando la conversión falla — verifica siempre antes de usar. tostring() es seguro para cualquier tipo. type() devuelve el nombre del tipo como string para validación.
Constantes y convenciones
-- Lua no tiene const nativo -- Convención: UPPER_CASE para constantes local MAX_SIZE = 100 local PI = 3.14159 local APP_NAME = "MiApp" -- Convenciones de nombres: local miVariable = 10 -- camelCase local mi_variable = 10 -- snake_case local _privado = "interno" -- prefijo _ -- Nombres reservados (keywords): -- and, break, do, else, elseif, end -- false, for, function, goto, if -- in, local, nil, not, or, repeat -- return, then, true, until, while -- Evitar: nombres con _ mayúscula -- (reservado para variables internas)
Lua no tiene keyword const — usa la convención UPPER_CASE para valores que no cambian. Los nombres iniciados con _ indican uso interno/privado por convención. Keywords como local, function, return no pueden usarse como identificadores. Lua es case-sensitive.
Asignación múltiple
-- Asignación simultánea: local x, y, z = 1, 2, 3 local a, b = 10, 20 -- Swap sin variable temporal: a, b = b, a -- a=20, b=10 -- Los valores extra se descartan: local m, n = 1, 2, 3 -- 3 ignorado -- Los valores faltantes se vuelven nil: local p, q, r = 1, 2 -- r = nil -- Retornos múltiples de funciones: local ok, err = pcall(funcion)
La asignación múltiple evalúa todos los valores a la derecha antes de asignar — permite un swap elegante sin variable temporal. Los valores excedentes se descartan y los faltantes reciben nil. Es idiomática para capturar retornos múltiples de funciones como pcall e io.open.
Entrada y salida
-- Salida:
print("Hola Mundo") -- con newline
print("x:", 10, "y:", 20) -- tab entre args
io.write("sin newline") -- sin \n
-- Formateo:
print(string.format("%s tiene %d años", nombre, 30))
-- Entrada del usuario:
io.write("Nombre: ")
local nombre = io.read() -- línea completa
local num = io.read("*n") -- número
local todo = io.read("*a") -- fichero entero
-- Argumentos de línea de comandos:
-- lua script.lua arg1 arg2
print(arg[0]) -- "script.lua"
print(arg[1]) -- "arg1"print() acepta múltiples argumentos separados por tab y añade newline automáticamente. io.write() da control fino (sin newline). io.read() sin argumentos lee una línea; "*n" parsea un número directamente. La table global arg contiene los argumentos CLI.
type() y verificación
-- type() devuelve el nombre del tipo:
type("hola") -- "string"
type(42) -- "number"
type(true) -- "boolean"
type(nil) -- "nil"
type({}) -- "table"
type(print) -- "function"
type(io.open) -- "function"
-- Validación por tipo:
local function procesar(x)
if type(x) ~= "number" then
error("espera number, recibió " .. type(x))
end
return x * 2
end
-- Dispatch por tipo:
local function describir(v)
local t = type(v)
if t == "number" then return "num: " .. v
elseif t == "string" then return "str: " .. v
else return t end
endtype() devuelve el nombre del tipo como string — esencial para validación y dispatch. Combínalo con error() para fallar rápido con un mensaje claro. Para verificar si es entero en Lua 5.3+, usa math.type(x) == "integer". tostring() y tonumber() complementan la introspección de tipos.
Números y matemáticas
-- Literales: local dec = 42 local float = 3.14 local hex = 0xFF -- 255 local exp = 1.5e3 -- 1500.0 -- Operadores (Lua 5.3+): 10 / 3 -- 3.333... (¡siempre float!) 10 // 3 -- 3 (división entera) 10 % 3 -- 1 (módulo) 10 ^ 3 -- 1000.0 (potencia, float) -- math library: math.floor(3.7) -- 3 math.ceil(3.2) -- 4 math.abs(-5) -- 5 math.max(1, 5, 3) -- 5 math.sqrt(16) -- 4.0 math.random(1, 100) -- entero 1-100 math.pi -- 3.14159...
En Lua, / es siempre división float — para división entera usa // (Lua 5.3+). ^ devuelve siempre float. Desde Lua 5.3 existen los subtipos integer (64-bit) y float (double), con conversión automática. Usa math.randomseed para reproducibilidad.
Ámbito y bloques
-- Bloques (do...end): do local temp = 42 print(temp) -- 42 end print(temp) -- ¡nil! (fuera de scope) -- Shadowing: local x = 1 do local x = 2 -- nuevo x, oculta el outer print(x) -- 2 end print(x) -- 1 -- Upvalues (base de closures): local contador = 0 local function inc() contador = contador + 1 -- upvalue end -- _ENV (entorno en Lua 5.2+): -- las globales son campos de _ENV
Lua usa scope léxico con bloques delimitados por do...end, if...end, for...end. Un local dentro de un bloque muere al final — previene la contaminación del namespace. Los upvalues son variables locales capturadas por funciones anidadas. Desde Lua 5.2, _ENV permite sandboxing.
Strings básicas
-- Comillas simples o dobles:
local s1 = "Hola Mundo"
local s2 = 'También funciona'
-- String larga (multilínea):
local texto = [[
Línea 1
Línea 2 sin escape
]]
-- Concatenación (..):
local msg = "Hola, " .. nombre .. "!"
local n = "edad: " .. 30 -- auto-convert
-- Longitud:
#s1 -- 10 (bytes)
-- Repetición:
string.rep("ab", 3) -- "ababab"Las strings en Lua son inmutables — las operaciones crean strings nuevas. El operador .. concatena y convierte números automáticamente. Las strings largas [[ ]] preservan el formato sin escapes — ideales para templates. #string devuelve la longitud en bytes (no caracteres UTF-8).
nil y truthiness
-- nil: ausencia de valor
local x = nil
print(x) -- nil
-- FALSO en Lua: ¡SOLO nil y false!
if nil then end -- no se ejecuta
if false then end -- no se ejecuta
-- VERDADERO: ¡todo lo demás!
if 0 then print("¡0 es true!") end
if "" then print("¡vacío es true!") end
if {} then print("¡{} es true!") end
-- nil como "eliminación":
local t = {a = 1, b = 2}
t.a = nil -- "elimina" el campo a
-- Verificar existencia:
if t.b ~= nil then
print("b existe")
endEn Lua, solo nil y false son falsy — 0, string vacía y table vacía son TODOS verdaderos (a diferencia de Python/JS). nil se usa para "eliminar" campos de tables e indicar ausencia de valor. Usa siempre ~= nil para verificar existencia explícita.
Funções
Definición y llamada
-- Función local (recomendado):
local function saludo(nombre)
return "Hola, " .. nombre .. "!"
end
print(saludo("Ana")) -- "Hola, Ana!"
-- Sintaxis alternativa:
local doble = function(x)
return x * 2
end
-- Sin return: devuelve nil
local function log(msg)
print("[LOG] " .. msg)
end
-- Función global (evitar):
function global_fn()
return 42
end
-- Llamada con sintaxis especial:
print "Hola" -- sin paréntesis (1 arg string)
dofile "script.lua"Las funciones en Lua son valores de primera clase — pueden almacenarse, pasarse y devolverse. local function es preferible a global. Sin return explícito, la función devuelve nil. Lua permite omitir paréntesis cuando el único argumento es una string literal o table constructor: print "Hola".
Funciones como valores
-- Dispatch table:
local ops = {
suma = function(a, b) return a + b end,
sub = function(a, b) return a - b end,
mul = function(a, b) return a * b end,
}
print(ops.suma(3, 4)) -- 7
-- Higher-order function:
local function aplicar(fn, lista)
local resultado = {}
for i, v in ipairs(lista) do
resultado[i] = fn(v)
end
return resultado
end
local dobles = aplicar(function(x)
return x * 2
end, {1, 2, 3})
-- Callback en sort:
table.sort(nums, function(a, b)
return a > b -- descendente
end)Las funciones como valores de primera clase permiten dispatch tables, higher-order functions y callbacks. table.sort con comparador custom es el ejemplo más común. La sintaxis obj:método() es azúcar para obj.método(obj) — pasa self automáticamente. Las funciones anónimas inline son ubicuas en Lua.
Métodos y self (:)
-- Definición con : (self implícito):
local Cuenta = {}
Cuenta.__index = Cuenta
function Cuenta.new(saldo)
return setmetatable({saldo = saldo}, Cuenta)
end
-- : pasa self automáticamente:
function Cuenta:depositar(v)
self.saldo = self.saldo + v
end
function Cuenta:get_saldo()
return self.saldo
end
-- Llamada con :
local c = Cuenta.new(100)
c:depositar(50)
print(c:get_saldo()) -- 150
-- Equivalencia:
-- c:depositar(50) == c.depositar(c, 50)
-- function Cuenta:fn() == function Cuenta.fn(self)La sintaxis : (dos puntos) pasa self automáticamente: obj:método() equivale a obj.método(obj). En la definición, function Obj:fn() equivale a function Obj.fn(self). Es la convención universal para métodos en Lua — sin :, self sería nil y el método fallaría.
Retornos múltiples
-- Devolver múltiples valores:
local function dividir(a, b)
if b == 0 then
return nil, "División por cero"
end
return a / b, nil
end
local resultado, err = dividir(10, 3)
if err then
print("Error: " .. err)
else
print("Resultado: " .. resultado)
end
-- Patrón io.open:
local f, err = io.open("file.txt", "r")
if not f then
print("Fallo: " .. err)
return
end
-- Ajustar el número de retornos:
local a, b = funcion() -- captura 2
local x = funcion() -- captura solo 1
local t = {funcion()} -- captura todosLos retornos múltiples son idiomáticos en Lua: el patrón valor, error (nil + mensaje en fallo) evita excepciones para errores esperados. io.open, pcall y string.find usan este patrón. La asignación captura exactamente el número de variables — usa {fn()} para capturar todos en una table.
Recursión y tail calls
-- Recursión simple: local function factorial(n) if n <= 1 then return 1 end return n * factorial(n - 1) end -- Tail call (¡optimizada por Lua!): local function fat_tail(n, acc) acc = acc or 1 if n <= 1 then return acc end return fat_tail(n - 1, n * acc) -- ^ tail call: ¡sin stack extra! end -- NO es tail call (operación después): local function fat_bad(n) if n <= 1 then return 1 end return n * fat_bad(n - 1) -- ¡* después! end -- print(fat_tail(100000)) -- ¡OK! -- print(fat_bad(100000)) -- ¡stack overflow!
Lua optimiza tail calls: si la última acción es return func(args), la stack frame se reutiliza — recursión infinita sin overflow. Para ser tail call, el return debe ser EXACTAMENTE la llamada — sin operaciones posteriores. Esto permite loops vía recursión con rendimiento de iteración. Esencial para algoritmos recursivos profundos.
Varargs (...)
-- Número variable de argumentos:
local function suma(...)
local args = {...} -- table de los args
local total = 0
for _, v in ipairs(args) do
total = total + v
end
return total
end
print(suma(1, 2, 3, 4)) -- 10
-- select para acceder a ...:
local n = select("#", ...) -- count seguro
local primero = select(1, ...) -- 1er arg
-- table.pack preserva nils (5.3+):
local safe = table.pack(...)
print(safe.n) -- count real
-- Reenviar argumentos:
local function log(fn, ...)
print("llamando...")
return fn(...) -- reenvía todos
end
-- printf-style:
local function printf(fmt, ...)
io.write(string.format(fmt, ...))
end... (varargs) captura argumentos extra. {...} crea una table pero pierde los nils trailing — usa select("#", ...) o table.pack para un count seguro. fn(...) reenvía todos los argumentos. El patrón string.format(fmt, ...) es la base de funciones printf-style en Lua.
Iteradores customizados
-- Iterator con closure:
local function range(n)
local i = 0
return function()
i = i + 1
if i <= n then return i end
end
end
for i in range(5) do
print(i) -- 1, 2, 3, 4, 5
end
-- Pares ordenados:
local function sorted_pairs(t)
local keys = {}
for k in pairs(t) do
keys[#keys + 1] = k
end
table.sort(keys)
local i = 0
return function()
i = i + 1
local k = keys[i]
if k then return k, t[k] end
end
end
for k, v in sorted_pairs(config) do
print(k, v)
endLos iteradores en Lua son funciones que devuelven el siguiente valor en cada llamada — cuando devuelven nil, el for genérico para. Los closures con estado local son el patrón más común. El for genérico llama al iterador repetidamente hasta nil. Permiten secuencias infinitas, filtros y transformaciones lazy.
Closures
-- Closure: función + entorno capturado
local function crear_contador()
local count = 0 -- upvalue
return function()
count = count + 1
return count
end
end
local c1 = crear_contador()
local c2 = crear_contador()
print(c1()) -- 1
print(c1()) -- 2
print(c2()) -- 1 (¡independiente!)
-- Closure con parámetros:
local function multiplicador(factor)
return function(x)
return x * factor
end
end
local doble = multiplicador(2)
local triple = multiplicador(3)
print(doble(5)) -- 10
print(triple(5)) -- 15Los closures capturan upvalues (variables locales del scope exterior) por referencia — cada llamada crea un entorno independiente. Permiten estado privado sin clases: las variables locales son inaccesibles externamente. Son la principal herramienta de encapsulamiento en Lua y la base de callbacks, iterators y módulos.
pcall y xpcall
-- pcall (protected call):
local ok, resultado = pcall(function()
return operacion_peligrosa()
end)
if ok then
print("Éxito: " .. resultado)
else
print("Error: " .. resultado)
end
-- pcall con función directa:
local ok, err = pcall(io.open, "file.txt")
-- xpcall con handler custom:
local ok, err = xpcall(funcion, function(e)
return e .. "\n" .. debug.traceback("", 2)
end)
-- assert (error si falso):
local f = assert(io.open("datos.txt"))
local n = assert(tonumber(input), "No es número")
-- error() lanza error:
if edad < 0 then
error("Edad negativa", 2)
endpcall ejecuta en modo protegido: devuelve true+resultado en éxito, false+mensaje en error — es el try/catch de Lua. xpcall añade handler custom (ej: stack trace vía debug.traceback). assert() es azúcar para "si falso, error()". Usa error() para errores de programación.
Controlo de Fluxo
If / Elseif / Else
-- Condicional básico:
if nota >= 18 then
print("Excelente")
elseif nota >= 10 then
print("Aprobado")
else
print("Suspendido")
end
-- elseif es keyword única (no else if)
-- Sin switch/case nativo:
-- Usa if/elseif o dispatch table:
local acciones = {
["start"] = function() iniciar() end,
["stop"] = function() parar() end,
}
local fn = acciones[comando]
if fn then fn() endelseif es una keyword única en Lua (no un else if separado). No hay switch/case nativo — usa cadenas if/elseif o dispatch tables (mapear comandos a funciones). La condición puede ser cualquier valor: solo nil y false no ejecutan el bloque.
For genérico (ipairs/pairs)
-- ipairs: arrays en orden secuencial
local frutas = {"manzana", "banana", "uva"}
for i, v in ipairs(frutas) do
print(i, v) -- 1 manzana, 2 banana...
end
-- ¡PARA en el primer nil!
-- pairs: todos los pares (sin orden)
local persona = {nombre="Ana", edad=30}
for k, v in pairs(persona) do
print(k, v)
end
-- Iterar solo claves:
for k in pairs(persona) do
print(k)
end
-- next() manualmente:
local k, v = next(persona)
-- Iterar fichero línea a línea:
for linea in io.lines("datos.txt") do
print(linea)
endipairs itera arrays en orden 1,2,3... y PARA en el primer nil — ideal para listas secuenciales. pairs itera TODOS los pares clave-valor sin orden garantizado. El for genérico acepta cualquier iterador: io.lines, string.gmatch, coroutines. La variable de iteración es local y read-only.
Dispatch tables
-- Sustituye switch/case:
local handlers = {
["GET"] = function(req) return listar(req) end,
["POST"] = function(req) return crear(req) end,
["PUT"] = function(req) return actualizar(req) end,
["DELETE"] = function(req) return eliminar(req) end,
}
local function router(req)
local handler = handlers[req.method]
if not handler then
return 405, "Method not allowed"
end
return handler(req)
end
-- Con default:
local resultado = (handlers[cmd] or function()
print("Comando desconocido: " .. cmd)
end)(arg)
-- Registro dinámico:
handlers["PATCH"] = function(req)
return patch(req)
endLas dispatch tables mapean claves a funciones — sustituyen switch/case con ventajas: registro dinámico, lookup O(1), extensibilidad. El patrón (handlers[cmd] or default)(args) ejecuta con fallback. Son ubicuas en Lua: routers HTTP, parsers, command patterns y state machines.
While
-- while: condición ANTES del cuerpo
local n = 10
while n > 0 do
print(n)
n = n - 1
end
-- Loop infinito (game loop, servidor):
while true do
local evento = esperar_evento()
if evento == "salir" then break end
procesar(evento)
end
-- while con múltiples condiciones:
local i, j = 1, 100
while i < j do
i = i + 1
j = j - 1
end
-- Cuidado: puede no ejecutarse nunca
local x = 0
while x > 10 do
print("nunca aparece")
endwhile verifica la condición ANTES de ejecutar — el cuerpo puede no ejecutarse nunca si la condición es falsa inicialmente. Para loops infinitos, usa while true do con break para salir. La condición se reevalúa en cada iteración. Las variables del cuerpo son locales a cada iteración.
Break y goto
-- break: sale del loop más interno
for i = 1, 100 do
if i * i > 50 then
print("encontrado: " .. i)
break
end
end
-- break debe ser la última statement:
while true do
if condicion then
break -- OK (fin del bloque if)
end
end
-- goto (Lua 5.2+): salir de loops anidados
for i = 1, 10 do
for j = 1, 10 do
if i * j > 50 then
goto found
end
end
end
::found::
print("¡Encontrado!")
-- goto para retry:
::intentar::
local ok, err = pcall(operacion)
if not ok then goto intentar endbreak solo puede ser la última statement de un bloque — sale solo del loop más interno. goto (Lua 5.2+) permite saltar a labels ::nombre:: — útil para break de loops anidados y patrones de retry. Los labels no pueden saltar sobre declaraciones local. Úsalo con moderación.
Repeat-Until
-- repeat-until: condición DESPUÉS del cuerpo
-- ¡Se ejecuta al menos 1 vez!
local input
repeat
io.write("Contraseña: ")
input = io.read()
until input == "secreta"
-- Ventaja: variables visibles en el until
repeat
local x = calcular()
until x > 100 -- ¡x visible aquí!
-- El while equivalente necesitaría:
local x
while true do
x = calcular()
if x > 100 then break end
end
-- until invierte: para cuando TRUE
-- (continúa mientras FALSE)repeat-until se ejecuta al menos una vez (condición verificada DESPUÉS). Gran ventaja: las variables declaradas en el cuerpo son visibles en la condición until. La lógica está invertida — el loop PARA cuando la condición es verdadera. Ideal para validación de input y loops "haz hasta lograrlo".
Patrones de loop
-- Acumulador:
local suma = 0
for i = 1, 100 do
suma = suma + i
end
-- Búsqueda con flag:
local encontrado = false
for _, v in ipairs(lista) do
if v == objetivo then
encontrado = true
break
end
end
-- Filter manual:
local pares = {}
for _, v in ipairs(nums) do
if v % 2 == 0 then
pares[#pares + 1] = v
end
end
-- Loop con índice y valor:
for i, v in ipairs(items) do
print(i .. ": " .. v)
end
-- Countdown con step:
for i = #t, 1, -1 do
procesar(t[i])
endPatrones comunes: acumulador (suma/concatenación), búsqueda con break, filter manual con #t+1 para append. Iterar de atrás hacia delante (for i = #t, 1, -1) es seguro para eliminar durante la iteración. Lua no tiene map/filter/reduce built-in — se implementan con loops simples.
For numérico
-- for i = inicio, fin, paso:
for i = 1, 10 do
print(i) -- 1, 2, ..., 10
end
-- Con paso negativo (countdown):
for i = 10, 1, -1 do
print(i)
end
-- Con paso decimal:
for i = 0, 1, 0.1 do
print(string.format("%.1f", i))
end
-- La variable es LOCAL y read-only:
for i = 1, 5 do
i = i * 2 -- ¡NO afecta al loop!
end
-- inicio, fin, paso se evalúan 1 vez:
local limite = 10
for i = 1, limite do
limite = 0 -- no afecta las iteraciones
endEl for numérico tiene la forma for i = inicio, fin, paso do. El paso default es 1; puede ser negativo o decimal. La variable de iteración es local y read-only — reasignarla no afecta al loop. Inicio, fin y paso se evalúan UNA vez antes de empezar. El loop incluye el valor final (es inclusivo).
Guard clauses y early return
-- En vez de nesting profundo:
local function procesar(datos)
if not datos then return nil, "sin datos" end
if #datos == 0 then return {} end
if type(datos[1]) ~= "number" then
return nil, "tipo inválido"
end
-- Lógica principal (sin nesting):
local resultado = {}
for _, v in ipairs(datos) do
resultado[#resultado + 1] = v * 2
end
return resultado
end
-- Validación con assert:
local function dividir(a, b)
assert(type(a) == "number", "a debe ser number")
assert(b ~= 0, "división por cero")
return a / b
endLas guard clauses verifican condiciones de error al inicio y hacen return temprano — evitan nesting profundo y hacen el código más legible. El patrón return nil, msg para errores operacionales es idiomático. assert() valida precondiciones de programación (falla rápido). Combínalo con pcall en el llamador para capturar.
Strings e Patterns
string library
-- Operaciones básicas:
local s = "Hola Mundo"
string.upper(s) -- "HOLA MUNDO"
string.lower(s) -- "hola mundo"
string.len(s) -- 10 (bytes)
string.rep("ab", 3) -- "ababab"
string.reverse("abc") -- "cba"
-- Substring:
string.sub(s, 1, 4) -- "Hola"
string.sub(s, 6) -- "Mundo" (hasta el fin)
string.sub(s, -5) -- "Mundo" (desde el fin)
-- Byte/char:
string.byte("A") -- 65
string.char(65, 66) -- "AB"
-- Método con : (sugar):
local u = s:upper() -- equivalente
local p = s:sub(1, 4)La biblioteca string ofrece operaciones inmutables — todas devuelven una nueva string. Puede llamarse como función (string.upper(s)) o como método (s:upper()). string.sub acepta índices negativos (desde el fin). string.byte/string.char convierten entre bytes y caracteres. Las strings siempre son inmutables en Lua.
string.gsub
-- Sustitución global:
local s = "Hola Mundo Mundo"
local r, n = string.gsub(s, "Mundo", "Lua")
print(r) -- "Hola Lua Lua"
print(n) -- 2 (sustituciones)
-- Limitar sustituciones:
string.gsub(s, "Mundo", "Lua", 1)
-- "Hola Lua Mundo" (solo 1ª)
-- Con patterns:
string.gsub("a1b2c3", "%d", "#")
-- "a#b#c#"
-- Con función (transformación):
string.gsub("hello world", "%a+", function(w)
return w:upper()
end)
-- "HELLO WORLD"
-- Con table (lookup):
local entities = {lt="<", gt=">", amp="&"}
string.gsub("<>", "&(%a+);", entities)
-- "<>"string.gsub sustituye todas las ocurrencias y devuelve la string + count. El 4º argumento limita las sustituciones. Puede recibir una función (el resultado de la función sustituye) o una table (clave → valor) como reemplazo. Es la herramienta más poderosa de transformación de strings en Lua — sustituye el regex replace de otros lenguajes.
string.format
-- Formato estilo printf:
string.format("%s tiene %d años", "Ana", 30)
-- "Ana tiene 30 años"
-- Especificadores:
string.format("%.2f", 3.14159) -- "3.14"
string.format("%10s", "Lua") -- " Lua"
string.format("%-10s|", "Lua") -- "Lua |"
string.format("%05d", 42) -- "00042"
string.format("%x", 255) -- "ff"
string.format("%X", 255) -- "FF"
string.format("%o", 8) -- "10" (octal)
string.format("%c", 65) -- "A"
string.format("%q", 'he said "hi"')
-- "he said \"hi\"" (escapado)
-- %% para porcentaje literal:
string.format("%.1f%%", 85.678) -- "85.7%"
-- %d solo integers (5.3+):
string.format("%d", 3.7) -- ¡ERROR!string.format es el printf de Lua: %d entero, %s string, %f float, %x hex, %c char. %.2f limita los decimales. %q escapa a un literal Lua seguro. %% para porcentaje literal. En Lua 5.3+, %d exige integer — usa %f o math.floor para floats.
Capturas y parsing
-- Capturas con paréntesis:
local key, val = string.match(
"nombre=Ana", "(%a+)=(%a+)")
print(key, val) -- "nombre", "Ana"
-- Parsing de URL:
local proto, host, path = string.match(
"https://site.com/page",
"(%a+)://([^/]+)(.*)")
-- CSV simple:
for campo in string.gmatch("a,b,c", "[^,]+") do
print(campo) -- a, b, c
end
-- gmatch: iterador de matches
for word in string.gmatch("one two three", "%a+") do
print(word)
end
-- Captura posicional:
local h, m = string.match("14:30", "(%d+):(%d+)")
print(h, m) -- 14, 30
-- %0 captura el match entero:
string.gsub("abc", "(%a)", "[%0]")
-- "[a][b][c]"Las capturas con () extraen partes del match — string.match devuelve todas las capturas. string.gmatch es un iterador sobre todos los matches (ideal para parsing de CSV, tokens). %0 se refiere al match entero. Patterns como [^,]+ (uno o más no-coma) son comunes para splitting. Más simple que regex para casos típicos.
string.find y match
-- find: devuelve posiciones (o nil)
local ini, fin = string.find("Hola Mundo", "Mundo")
print(ini, fin) -- 6, 10
-- Con patterns:
local i, j = string.find("precio: 42", "%d+")
print(i, j) -- 9, 10
-- match: extrae el patrón encontrado
local num = string.match("edad: 30", "%d+")
print(num) -- "30"
-- Múltiples capturas:
local d, m, a = string.match(
"25/12/2024", "(%d+)/(%d+)/(%d+)")
print(d, m, a) -- 25, 12, 2024
-- Sin match: devuelve nil
local r = string.match("abc", "%d+")
print(r) -- nil
-- plain text (sin patterns):
string.find("a.b", ".", 1, true) -- 2, 2string.find devuelve posiciones inicio/fin (o nil). string.match extrae el contenido encontrado — más conveniente para parsing. Las capturas con () devuelven múltiples valores. El 4º argumento true en find desactiva los patterns (búsqueda literal). Ambos aceptan una posición inicial como argumento extra.
string.gmatch y split
-- gmatch: iterador de patterns
for word in string.gmatch("uno dos tres", "%S+") do
print(word)
end
-- Split por delimitador:
local function split(s, sep)
local parts = {}
for part in string.gmatch(s, "[^" .. sep .. "]+") do
parts[#parts + 1] = part
end
return parts
end
local t = split("a,b,c", ",")
-- {"a", "b", "c"}
-- Tokenizer simple:
for token in string.gmatch(code, "%a+%d*") do
print(token)
end
-- Extraer todos los números:
local nums = {}
for n in string.gmatch("x1 y22 z333", "%d+") do
nums[#nums + 1] = tonumber(n)
end
-- {1, 22, 333}string.gmatch devuelve un iterador sobre todos los matches — ideal para splitting, tokenizing y extracción múltiple. El patrón [^sep]+ (no-delimitador) es la base de las funciones split. %S+ captura palabras (no-espacio). A diferencia de match (solo el primero), gmatch recorre la string entera. Esencial para parsing en Lua.
Patterns en Lua
-- Patterns (¡NO son regex!):
-- %a = letra, %d = dígito, %s = espacio
-- %w = alfanumérico, %p = puntuación
-- %u = mayúscula, %l = minúscula
-- Cuantificadores:
-- + = 1 o más (greedy)
-- * = 0 o más (greedy)
-- - = 0 o más (lazy)
-- ? = 0 o 1
-- Anclas:
-- ^ = inicio de la string
-- $ = fin de la string
-- Ejemplos:
string.match("abc123", "%a+") -- "abc"
string.match("abc123", "%d+") -- "123"
string.match("a.b", "%.") -- "." (escape)
string.match("hello", "^h") -- "h"
string.match("file.lua", "%.%w+$") -- "lua"
-- Clases mayúsculas = complemento:
-- %A = no-letra, %D = no-dígitoLos patterns de Lua son más simples que las regex: %a letra, %d dígito, %s espacio, %w alfanumérico. Cuantificadores: + (greedy), - (lazy), *, ?. % escapa caracteres especiales. Las clases mayúsculas son complementos (%D = no-dígito). No soportan alternancia (|) ni grupos complejos.
utf8 (Lua 5.3+)
-- # devuelve BYTES, no caracteres:
#"más" -- 4 (á = 2 bytes UTF-8)
-- utf8 library (5.3+):
local utf8 = require("utf8") -- o built-in
-- Longitud en caracteres:
utf8.len("más") -- 3
-- Iterar codepoints:
for pos, code in utf8.codes("más") do
print(pos, code)
end
-- Convertir:
utf8.char(65, 66) -- "AB"
utf8.codepoint("á") -- 225
-- Offset (posición del byte):
utf8.offset("más", 3) -- byte pos del 3er char
-- Substring por carácter:
local function usub(s, i, j)
local pi = utf8.offset(s, i)
local pj = utf8.offset(s, j + 1)
return s:sub(pi, pj - 1)
endEn Lua, # devuelve bytes — para contar caracteres UTF-8 usa utf8.len() (Lua 5.3+). utf8.codes() itera codepoints con posiciones. utf8.offset() convierte posición de carácter a byte (necesario para string.sub). Sin la biblioteca utf8, operaciones como upper/lower pueden fallar con caracteres acentuados.
Avançado
Coroutines
-- Crear y ejecutar:
local co = coroutine.create(function(a, b)
print("Inicio:", a, b)
local c = coroutine.yield(a + b)
print("Recibí:", c)
return a * b
end)
print(coroutine.resume(co, 3, 4))
-- "Inicio: 3 4", true, 7
print(coroutine.resume(co, 10))
-- "Recibí: 10", true, 12
print(coroutine.status(co)) -- "dead"
-- coroutine.wrap (generator simple):
local function fibonacci()
return coroutine.wrap(function()
local a, b = 0, 1
while true do
coroutine.yield(a)
a, b = b, a + b
end
end)
end
for n in fibonacci() do
if n > 100 then break end
print(n)
endLas coroutines son funciones que pausan (yield) y retoman (resume) — cooperativas, no paralelas. coroutine.create + resume/yield dan control total; coroutine.wrap devuelve una función iterable (más simple para generators). Estados: suspended, running, dead. Usos: generators infinitos, state machines, async simulado.
Metaprogramación
-- __index como función (properties):
local Config = setmetatable({}, {
__index = function(t, key)
local env = os.getenv(key:upper())
return env or "default_" .. key
end
})
print(Config.database_url) -- lee ENV
-- __newindex (validación):
local Strict = setmetatable({}, {
__newindex = function(t, k, v)
if type(v) ~= "number" then
error(k .. " debe ser número")
end
rawset(t, k, v)
end
})
Strict.x = 42 -- OK
Strict.y = "abc" -- ¡ERROR!
-- DSL con __index + __call:
local html = setmetatable({}, {
__index = function(_, tag)
return function(content)
return "<" .. tag .. ">" .. content
.. "</" .. tag .. ">"
end
end
})
print(html.div("Hola")) -- <div>Hola</div>Metaprogramación vía metamethods: __index como función crea computed properties, __newindex valida escrituras, __call transforma tables en DSLs callable. rawset/rawget son esenciales dentro de los metamethods para evitar recursión infinita. Este mecanismo permite OOP, frameworks y DSLs expresivas en Lua.
Módulos y require
-- Fichero: utils.lua
local M = {}
local function privado() -- no exportado
return "interno"
end
function M.suma(a, b)
return a + b
end
return M -- OBLIGATORIO devolver
-- Uso en otro fichero:
local utils = require("utils")
print(utils.suma(3, 4)) -- 7
-- require es cacheado (singleton):
local u1 = require("utils")
local u2 = require("utils")
print(u1 == u2) -- true (misma table)
-- package.path (dónde búsqueda):
print(package.path) -- "./?.lua;..."
-- Submódulos:
local json = require("cjson")require carga y ejecuta un módulo UNA vez (cacheado en package.loaded) — las llamadas siguientes devuelven la misma table. El módulo DEBE devolver una table o función. Las variables local son privadas — solo lo exportado en la table es público. package.path define dónde búsqueda (? = nombre del módulo).
C API y FFI
-- Lua C API (extensiones nativas):
-- Fichero mylib.c:
-- #include <lua.h>
-- #include <lauxlib.h>
--
-- static int l_suma(lua_State *L) {
-- double a = luaL_checknumber(L, 1);
-- double b = luaL_checknumber(L, 2);
-- lua_pushnumber(L, a + b);
-- return 1;
-- }
--
-- int luaopen_mylib(lua_State *L) {
-- lua_register(L, "suma", l_suma);
-- return 0;
-- }
-- LuaJIT FFI (¡sin C!):
local ffi = require("ffi")
ffi.cdef[[
int printf(const char *fmt, ...);
]]
ffi.C.printf("¡Hola %s!\n", "Lua")
-- Cargar módulo C:
local mylib = require("mylib")La C API permite extender Lua con código nativo — las funciones C reciben lua_State (stack virtual), leen args con luaL_check*, devuelven vía lua_push*. luaopen_nombre es el entry point del require. LuaJIT FFI elimina C: ffi.cdef declara firmas y ffi.C llama funciones del sistema. Lua fue diseñada para embedding.
File I/O
-- Leer fichero completo:
local f = assert(io.open("datos.txt", "r"))
local contenido = f:read("*a")
f:close()
-- Leer línea a línea:
for linea in io.lines("datos.txt") do
print(linea)
end
-- Escribir:
local out = assert(io.open("salida.txt", "w"))
out:write("Línea 1\n")
out:write(string.format("Valor: %d\n", 42))
out:close()
-- Append:
local log = io.open("app.log", "a")
log:write(os.date() .. " evento\n")
log:close()
-- Modos: r, w, a, r+, w+, a+
-- rb, wb (binario en Windows)
-- Verificar existencia:
local function existe(path)
local f = io.open(path, "r")
if f then f:close() return true end
return false
endio.open devuelve file handle o nil + error — usa siempre assert() o verifica. io.lines() es el iterador idiomático para lectura línea a línea (cierra automáticamente). Modos: "r" lectura, "w" escritura (trunca), "a" append. En Windows, añade "b" para binario.
Debug y profiling
-- debug library:
local info = debug.getinfo(1)
print(info.source) -- "@script.lua"
print(info.currentline) -- línea actual
-- Stack trace:
print(debug.traceback("mensaje"))
-- Hooks (monitorizar ejecución):
debug.sethook(function(event, line)
print(event, line)
end, "l") -- "l" = cada línea
-- Inspeccionar upvalues:
local f = function()
local x = 42
return x
end
local name, val = debug.getupvalue(f, 1)
print(name, val) -- "x", 42
-- Profiling simple:
local function profile(fn, ...)
local start = os.clock()
local results = {fn(...)}
print(string.format("%.4fs", os.clock() - start))
return table.unpack(results)
end
-- Memoria:
collectgarbage("count") -- KB usadosLa biblioteca debug permite introspección: getinfo devuelve fuente/línea, traceback genera stack traces, getupvalue/getlocal inspeccionan variables. sethook permite profilers (evento "l" = cada línea, "c" = call). os.clock() mide CPU time. collectgarbage("count") reporta memoria en KB.
Error handling avanzado
-- Patrón result/error (Go-style):
local function leer_fichero(path)
local f, err = io.open(path, "r")
if not f then
return nil, "No abrí: " .. err
end
local content = f:read("*a")
f:close()
return content
end
local data, err = leer_fichero("config.json")
if not data then
print("Error: " .. err)
return
end
-- Error objects (estructurados):
error({code = 404, msg = "Not found"})
-- Retry con pcall:
local function con_retry(fn, intentos)
intentos = intentos or 3
for i = 1, intentos do
local ok, result = pcall(fn)
if ok then return result end
if i == intentos then error(result) end
end
endLua tiene dos estilos: excepciones (error/pcall) para errores de programación, y return nil, msg para errores operacionales. El estilo nil+msg es idiomático en I/O. Los error objects (tables con code/msg) permiten errores estructurados. con_retry encapsula intentos con pcall — patrón común en red.
OS y entorno
-- Fecha y hora:
os.time() -- timestamp Unix
os.date("%Y-%m-%d") -- "2024-12-25"
os.date("*t") -- table con campos
os.clock() -- CPU time (segundos)
os.difftime(t2, t1) -- diferencia
-- Entorno:
os.getenv("HOME") -- variable de entorno
os.getenv("PATH")
-- Sistema:
os.execute("ls -la") -- ejecutar comando
os.remove("temp.txt") -- borrar fichero
os.rename("a.txt", "b.txt")
os.tmpname() -- nombre temp único
os.exit(0) -- terminar programa
-- Tabla de fecha:
local t = os.date("*t")
print(t.year, t.month, t.day)
print(t.hour, t.min, t.sec)
-- Timestamp de fecha específica:
local ts = os.time({year=2024, month=12, day=25})os.time() devuelve timestamp Unix; os.date() formatea (con "*t" devuelve table con campos). os.clock() mide CPU time para profiling. os.getenv() lee variables de entorno. os.execute() ejecuta comandos del sistema. Para operaciones avanzadas de filesystem (mkdir, glob), usa LuaFileSystem (lfs) vía luarocks.
Operadores
Operadores aritméticos
-- Básicos: 10 + 3 -- 13 10 - 3 -- 7 10 * 3 -- 30 10 / 3 -- 3.333... (¡float!) -10 -- negación unaria -- Lua 5.3+: 10 // 3 -- 3 (división entera) 10 % 3 -- 1 (módulo/resto) 10 ^ 3 -- 1000.0 (potencia) -- Módulo con negativos: -7 % 3 -- 2 (resultado positivo) 7 % -3 -- -2 (signo del divisor) -- Integer vs float: local a = 10 // 3 -- integer 3 local b = 10 / 3 -- float 3.333 local c = 2 ^ 10 -- float 1024.0
/ es siempre float en Lua; // hace división entera (Lua 5.3+). % sigue el signo del divisor. ^ devuelve siempre float y es right-associative (2^3^2 = 2^9 = 512). Las operaciones entre integers permanecen integer excepto / y ^.
Precedencia de operadores
-- Orden (alta -> baja):
-- ^
-- not # - (unario)
-- * / // %
-- + -
-- ..
-- < > <= >= ~= ==
-- and
-- or
-- Ejemplos:
2 + 3 * 4 -- 14 (no 20)
2 ^ 3 ^ 2 -- 512 (right-assoc)
-2 ^ 2 -- -4 (unario después de ^)
not nil and 42 -- 42
-- Usa paréntesis para claridad:
local x = (a + b) * c
local y = not (x == 0)
-- .. es right-associative:
"a" .. "b" .. "c" -- "a" .. ("b" .. "c")^ tiene la mayor precedencia y es right-associative. El - unario tiene precedencia menor que ^ (-2^2 = -4). .. también es right-associative. and tiene precedencia mayor que or. Ante la duda, usa paréntesis — dejan clara la intención sin coste de rendimiento.
rawget, rawset y rawequal
-- rawget: lectura sin metamethods
local t = setmetatable({}, {
__index = function() return "default" end
})
t.x -- "default" (vía __index)
rawget(t, "x") -- nil (ignora __index)
-- rawset: escritura sin metamethods
local strict = setmetatable({}, {
__newindex = function()
error("¡prohibido!")
end
})
strict.x = 1 -- ¡ERROR!
rawset(strict, "x", 1) -- OK (ignora)
-- rawequal: comparación sin __eq
local mt = {__eq = function() return true end}
local a = setmetatable({}, mt)
local b = setmetatable({}, mt)
a == b -- true (vía __eq)
rawequal(a, b) -- false (referencia)
-- rawlen (5.2+): # sin __len
rawlen("texto") -- 5Las funciones raw* evitan los metamethods: rawget/rawset son esenciales dentro de __index/__newindex para evitar recursión infinita. rawequal compara por referencia ignorando __eq. rawlen (5.2+) ignora __len. Úsalas solo cuando necesites acceder a la table "real".
Operadores relacionales
-- Comparaciones (devuelven boolean):
10 == 10 -- true (igual)
10 ~= 5 -- true (DIFERENTE, no !=)
10 < 20 -- true
10 > 5 -- true
10 <= 10 -- true
10 >= 11 -- false
-- NOTA: ¡~= es "diferente" en Lua!
-- (no existe !=)
-- Comparación de strings (lexicográfica):
"abc" < "abd" -- true
"Lua" == "Lua" -- true
-- Tables/functions: solo == y ~=
-- (comparan referencia, no contenido)
local a = {}
local b = {}
a == b -- false (objetos diferentes)
a == a -- true (misma referencia)El operador de desigualdad en Lua es ~= (no !=). Las comparaciones < y > funcionan para números y strings (orden lexicográfico). Tables y functions solo pueden compararse con ==/~= por referencia — para comparación por valor, usa los metamethods __eq, __lt.
Operadores bitwise (5.3+)
-- Bitwise (Lua 5.3+):
0xFF & 0x0F -- 15 (AND)
0xF0 | 0x0F -- 255 (OR)
0xFF ~ 0x0F -- 240 (XOR)
~0xFF -- -256 (NOT, complemento)
-- Shifts:
1 << 4 -- 16 (shift left)
256 >> 4 -- 16 (shift right)
-- Aplicaciones:
-- Flags/permissions:
local READ = 1 -- 001
local WRITE = 2 -- 010
local EXEC = 4 -- 100
local perms = READ | WRITE -- 3 (011)
-- Verificar flag:
if perms & READ ~= 0 then
print("tiene lectura")
endLos operadores bitwise existen desde Lua 5.3: & (AND), | (OR), ~ (XOR binario / NOT unario), << y >> (shifts). Operan solo sobre integers. Útiles para flags, permisos y manipulación de bits. En Lua 5.1/5.2 usa bit32 o bit (LuaJIT).
Operadores lógicos
-- and: devuelve el 1º falso o el último:
true and 42 -- 42
nil and 42 -- nil
1 and 2 and 3 -- 3
-- or: devuelve el 1º verdadero o el último:
nil or "default" -- "default"
false or nil or 42 -- 42
1 or 2 -- 1
-- not: devuelve boolean:
not nil -- true
not false -- true
not 0 -- ¡false! (0 es true)
not "" -- ¡false! ("" es true)
not {} -- ¡false! ({} es true)
-- Cortocircuito:
-- and para en el 1º falso
-- or para en el 1º verdaderoand y or en Lua NO devuelven boolean — devuelven el operando: and devuelve el primer falso o el último; or devuelve el primer verdadero o el último. Ambos hacen cortocircuito. not es el único que devuelve siempre true/false.
Patrón ternario (and/or)
-- Ternario idiomático:
local status = activo and "online" or "offline"
local nombre = input ~= "" and input or "Anónimo"
-- Equivalente a:
-- status = activo ? "online" : "offline"
-- CUIDADO: ¡falla si A es false/nil!
local x = true and false or "ups"
-- x = "ups" (¡no false!)
-- Solución con table:
local r = (cond and {valorA} or {valorB})[1]
-- Default value (más seguro):
local timeout = opts.timeout or 30
local host = config.host or "localhost"
-- Ejecución condicional:
debug and print("modo debug activo")El patrón cond and A or B simula un ternario — funciona cuando A nunca es false/nil. Si A puede ser falso, usa el truco de la table (cond and {A} or {B})[1]. Para defaults, x = valor or default es el patrón más común y seguro (equivalente al ?? de otros lenguajes).
Concatenación y length
-- Concatenación (..):
"Hola" .. " " .. "Mundo" -- "Hola Mundo"
"edad: " .. 30 -- "edad: 30"
"x=" .. 3.14 -- "x=3.14"
-- Atención: números pegados
print(10 .. 20) -- "1020" (¡string!)
print(10 + 20) -- 30 (suma)
-- Length (#):
#"texto" -- 5 (bytes)
#{1, 2, 3} -- 3 (array)
#"" -- 0
-- # con tables (parte secuencial):
local t = {10, 20, 30, nombre="x"}
#t -- 3 (solo parte array)
-- # NO funciona con nils en medio:
local bad = {1, nil, 3}
#bad -- ¡indefinido!El operador .. concatena strings y convierte números automáticamente — 10 .. 20 produce "1020". # devuelve la longitud de strings (bytes) y la parte secuencial de tables. Cuidado: # es indefinido en arrays con nil en medio — usa table.pack con el campo .n para contar con seguridad.
El operador # y tables
-- # en strings (bytes):
#"hello" -- 5
#"más" -- 4 (UTF-8: á = 2 bytes)
-- # en arrays (parte secuencial):
#{10, 20, 30} -- 3
#{} -- 0
-- Append usando #:
local t = {}
t[#t + 1] = "primero"
t[#t + 1] = "segundo"
-- PELIGRO: # con nils en medio
local bad = {1, 2, nil, 4}
#bad -- ¡puede ser 2 o 4!
-- Solución: table.pack preserva el count
local safe = table.pack(1, nil, 3)
print(safe.n) -- 3 (count real)
-- table.insert es más seguro:
table.insert(t, "tercero")#t devuelve el "border" de la parte secuencial — con nil en medio, el resultado es indefinido. Para append, t[#t+1] = v es idiomático pero table.insert(t, v) es más seguro. table.pack (5.3+) preserva los nils con el campo .n. Nunca confíes en # para arrays dispersos.
Tables
Tables como arrays
-- Creación (¡índice 1-based!):
local frutas = {"manzana", "banana", "uva"}
print(frutas[1]) -- "manzana" (¡NO [0]!)
print(frutas[3]) -- "uva"
print(#frutas) -- 3
-- Construcción dinámica:
local nums = {}
for i = 1, 10 do
nums[#nums + 1] = i * i
end
-- Inserción y eliminación:
table.insert(frutas, "mango") -- final
table.insert(frutas, 1, "kiwi") -- posición
table.remove(frutas, 2) -- elimina pos 2
table.remove(frutas) -- elimina el último
-- Unpack:
local a, b, c = table.unpack({10, 20, 30})Las tables en Lua son 1-based — el primer elemento es [1], no [0]. #t devuelve la longitud de la parte secuencial. table.insert/table.remove manipulan posiciones; sin posición, operan al final. table.unpack esparce elementos como argumentos múltiples. Es la convención más importante al venir de otros lenguajes.
table.concat y pack
-- concat (unir con separador):
local palabras = {"Lua", "es", "rápida"}
print(table.concat(palabras, " "))
-- "Lua es rápida"
print(table.concat(palabras, ", ", 1, 2))
-- "Lua, es"
-- Eficiente para strings grandes:
local partes = {}
for i = 1, 1000 do
partes[#partes + 1] = "línea " .. i
end
local texto = table.concat(partes, "\n")
-- table.pack (5.3+, preserva nils):
local t = table.pack(1, nil, 3)
print(t.n) -- 3 (¡count real!)
local a, b, c = table.unpack(t, 1, t.n)
-- table.move (copiar ranges, 5.3+):
local arr = {1, 2, 3, 4, 5}
table.move(arr, 2, 4, 1) -- shift lefttable.concat es la forma eficiente de construir strings grandes — la concatenación repetida con .. es O(n²). table.pack preserva los nils con el campo .n ({...} pierde los nils trailing). table.move (5.3+) copia ranges eficientemente. Usa concat siempre que construyas output con muchas partes.
Filter, map y reduce
-- Lua no tiene built-in, implementarlos:
-- Filter:
local function filter(t, pred)
local result = {}
for _, v in ipairs(t) do
if pred(v) then
result[#result + 1] = v
end
end
return result
end
-- Map:
local function map(t, fn)
local result = {}
for i, v in ipairs(t) do
result[i] = fn(v)
end
return result
end
-- Reduce:
local function reduce(t, fn, init)
local acc = init
for _, v in ipairs(t) do
acc = fn(acc, v)
end
return acc
end
local suma = reduce({1,2,3,4}, function(a, b)
return a + b
end, 0) -- 10Lua no tiene map/filter/reduce built-in — se implementan como funciones utilitarias con loops. El patrón #result+1 para append es idiomático. Bibliotecas como Moses o lua-functional añaden estas funciones con chaining. La construcción manual es simple y eficiente — muchas veces preferible en Lua por claridad.
Tables como diccionarios
-- Creación con claves:
local persona = {
nombre = "Ana",
edad = 30,
["e-mail"] = "ana@mail.com",
}
-- Acceso:
print(persona.nombre) -- dot notation
print(persona["e-mail"]) -- bracket (claves especiales)
persona.ciudad = "Lisbon" -- añadir
persona.edad = nil -- "eliminar"
-- Iteración (sin orden garantizado):
for clave, valor in pairs(persona) do
print(clave .. ": " .. tostring(valor))
end
-- Verificar existencia:
if persona.email ~= nil then
print("tiene email")
end
-- Cualquier valor excepto nil como clave:
local t = {[true] = "yes", [42] = "answer"}Las tables como diccionarios usan cualquier valor excepto nil como clave. La dot notation (t.campo) solo funciona para identificadores válidos — para claves especiales usa brackets (t["e-mail"]). pairs() itera todos los pares sin orden garantizado. Definir un campo como nil "elimina" la entrada.
Sets con tables
-- Set: table con valores true
local function nuevo_set(lista)
local set = {}
for _, v in ipairs(lista) do
set[v] = true
end
return set
end
local a = nuevo_set({1, 2, 3, 4, 5})
local b = nuevo_set({3, 4, 5, 6, 7})
-- Pertenencia O(1):
if a[3] then print("3 está en a") end
-- Unión:
local union = {}
for k in pairs(a) do union[k] = true end
for k in pairs(b) do union[k] = true end
-- Intersección:
local inter = {}
for k in pairs(a) do
if b[k] then inter[k] = true end
end
-- Eliminar duplicados:
local seen = {}
local unicos = {}
for _, v in ipairs(lista) do
if not seen[v] then
seen[v] = true
unicos[#unicos + 1] = v
end
endLos sets en Lua son tables con valores true — la verificación de pertenencia (set[x]) es O(1) vs O(n) de buscar en un array. Las operaciones de conjunto son loops sobre pairs(). El patrón "seen" elimina duplicados preservando el orden. Ideales para tracking de estado visitado (grafos, BFS) y verificación frecuente de pertenencia.
Tables anidadas
-- Estructuras JSON-like:
local datos = {
users = {
{ nombre = "Ana", edad = 30 },
{ nombre = "Bruno", edad = 25 },
},
meta = { total = 2, pagina = 1 },
}
print(datos.users[1].nombre) -- "Ana"
print(datos.meta.total) -- 2
-- Construcción dinámica:
local arbol = {}
arbol.hijos = {}
arbol.hijos[1] = {valor = "A", hijos = {}}
-- Acceso seguro (evitar nil error):
local nombre = datos.users[3]
and datos.users[3].nombre
or "desconocido"
-- Tables mixtas (array + hash):
local config = {
"localhost", -- [1]
8080, -- [2]
debug = true,
name = "server",
}Las tables son la ÚNICA estructura de datos en Lua — sirven como array, dict, objeto, módulo y registro. El anidamiento crea estructuras accedidas con notación encadenada. Cuidado: acceder a un campo de nil causa error — usa and para acceso seguro en rutas opcionales. Las tables mixtas combinan parte array y hash en la misma table.
Weak tables y caches
-- Weak table (no impide el GC):
local cache = setmetatable({}, {
__mode = "v" -- values débiles
})
-- Memoization con weak cache:
local memo = setmetatable({}, {__mode = "kv"})
local function fib(n)
if memo[n] then return memo[n] end
local result
if n < 2 then result = n
else result = fib(n-1) + fib(n-2) end
memo[n] = result
return result
end
-- Opciones de __mode:
-- "k" = claves débiles
-- "v" = valores débiles
-- "kv" = ambos débiles
-- Cuando el objeto original se libera,
-- la entrada desaparece automáticamenteLas weak tables permiten que el garbage collector elimine entradas cuando claves/valores no están referenciados en otro lugar. __mode = "v" hace los valores débiles, "k" las claves, "kv" ambos. Permiten caches sin memory leaks — cuando el objeto se libera, la entrada desaparece. Esenciales para memoization y object pools.
table.sort
-- Sort ascendente (in-place):
local nums = {5, 3, 8, 1, 9}
table.sort(nums) -- {1, 3, 5, 8, 9}
-- Sort con comparador:
table.sort(nums, function(a, b)
return a > b -- descendente
end)
-- Sort de records:
local personas = {
{nombre = "Ana", edad = 30},
{nombre = "Bruno", edad = 25},
{nombre = "Carla", edad = 28},
}
table.sort(personas, function(a, b)
return a.edad < b.edad
end)
-- Stable sort (añadir índice):
for i, v in ipairs(personas) do
v._idx = i
end
table.sort(personas, function(a, b)
if a.edad == b.edad then
return a._idx < b._idx
end
return a.edad < b.edad
end)table.sort es in-place y usa quicksort — el comparador debe devolver true si a va antes que b. La estabilidad NO está garantizada — para stable sort, añade el índice original como desempate. El comparador debe ser consistente (orden total). No modifiques la table durante el sort.
Swap-remove y rendimiento
-- Eliminación O(1) (sin preservar orden):
local function remove_fast(t, i)
t[i] = t[#t] -- el último a la posición
t[#t] = nil -- elimina el último
end
-- vs table.remove (O(n), preserva orden):
table.remove(t, i) -- shift de todos
-- Pre-asignación para rendimiento:
local buffer = {}
for i = 1, 10000 do
buffer[i] = 0 -- pre-asignar
end
-- Evitar # en loop caliente:
local len = #t -- calcular 1 vez
for i = 1, len do
procesar(t[i])
end
-- ipairs vs for numérico:
-- for i=1,#t es más rápido que ipairs
for i = 1, #t do
local v = t[i]
endSwap-remove sustituye el elemento por el último y elimina — O(1) cuando el orden no importa (esencial en game loops). table.remove es O(n) pues hace shift. En loops calientes, cachea #t fuera del loop. for i=1,#t es ligeramente más rápido que ipairs en Lua 5.3+. La pre-asignación evita rehashing.
Metatables e OOP
Metatables básico
-- La metatable define el comportamiento de tables:
local mt = {}
local t = setmetatable({}, mt)
-- setmetatable devuelve la propia table:
local obj = setmetatable({}, {})
-- getmetatable:
local mt2 = getmetatable(t)
-- __index (fallback de lectura):
local defaults = {color = "blanco", tamano = 10}
local o = setmetatable({}, {__index = defaults})
print(o.color) -- "blanco" (de defaults)
o.color = "azul" -- crea campo propio
print(o.color) -- "azul" (propio)
-- __newindex (interceptar escritura):
local strict = setmetatable({}, {
__newindex = function(t, k, v)
error("Campo no permitido: " .. k)
end
})Las metatables son el mecanismo de extensión de Lua — definen cómo se comportan las tables en las operaciones. __index se consulta cuando un campo NO existe — puede ser una table (delegation) o una función (computed). __newindex intercepta la escritura de campos nuevos. setmetatable devuelve la table, permitiendo chaining.
Patrón de clases
-- Patrón universal de clases en Lua:
local Animal = {}
Animal.__index = Animal
function Animal.new(nombre, sonido)
local self = setmetatable({}, Animal)
self.nombre = nombre
self.sonido = sonido or "..."
return self
end
function Animal:hablar()
return self.nombre .. ": " .. self.sonido .. "!"
end
function Animal:__tostring()
return "Animal(" .. self.nombre .. ")"
end
-- Uso:
local rex = Animal.new("Rex", "Guau")
print(rex:hablar()) -- "Rex: Guau!"
print(rex) -- "Animal(Rex)"
-- rex:hablar() == rex.hablar(rex)El patrón de clases usa __index apuntando a la propia table: los campos inexistentes en el objeto se buscan en la clase. Los métodos usan : que pasa self automáticamente. No hay clases nativas en Lua — este patrón prototype-based con metatables es la convención universal. Cada objeto es una table con metatable.
Mixin y composición
-- Mixin: copiar métodos a la clase
local Serializable = {}
function Serializable:to_json()
local parts = {}
for k, v in pairs(self) do
if type(v) ~= "function" then
parts[#parts + 1] = string.format(
'"%s":"%s"', k, tostring(v))
end
end
return "{" .. table.concat(parts, ",") .. "}"
end
local Loggable = {}
function Loggable:log(msg)
print("[" .. tostring(self) .. "] " .. msg)
end
-- Aplicar mixins:
local User = {}
User.__index = User
for k, v in pairs(Serializable) do User[k] = v end
for k, v in pairs(Loggable) do User[k] = v end
function User.new(nombre)
return setmetatable({nombre = nombre}, User)
endLa composición vía mixin copia métodos de módulos a clases — alternativa a la herencia múltiple sin la complejidad de cadenas __index. Ventajas: sin acoplamiento jerárquico, sin diamond problem, composición selectiva. Desventaja: los conflictos de nombres son silenciosos (el último en copiar gana). Prefiere composición cuando los comportamientos son independientes.
__index y delegation
-- __index como table (prototype):
local Animal = {sonido = "...", patas = 4}
local perro = setmetatable({}, {__index = Animal})
print(perro.sonido) -- "..." (heredado)
print(perro.patas) -- 4 (heredado)
perro.sonido = "Guau" -- campo propio
print(perro.sonido) -- "Guau" (override)
-- __index como función (computed):
local proxy = setmetatable({}, {
__index = function(t, key)
return "el campo '" .. key .. "' no existe"
end
})
print(proxy.xyz) -- "el campo 'xyz' no existe"
-- Cadena de prototypes:
local A = {nivel = "A"}
local B = setmetatable({}, {__index = A})
local C = setmetatable({}, {__index = B})
print(C.nivel) -- "A" (C -> B -> A)__index como table crea delegation/prototype — la lookup sigue la cadena hasta encontrar. Como función, permite computed properties (valores calculados on-demand). La cadena puede tener múltiples niveles (C → B → A). Es el mecanismo fundamental para OOP, módulos con defaults y configuración con fallback en Lua.
Herencia
-- Clase base:
local Animal = {}
Animal.__index = Animal
function Animal.new(nombre)
return setmetatable({nombre = nombre}, Animal)
end
function Animal:hablar()
return self.nombre .. " hace ..."
end
-- Clase derivada:
local Perro = setmetatable({}, {__index = Animal})
Perro.__index = Perro
function Perro.new(nombre, raza)
local self = Animal.new(nombre)
setmetatable(self, Perro)
self.raza = raza
return self
end
function Perro:hablar() -- override
return self.nombre .. ": ¡Guau guau!"
end
-- Cadena: Perro -> Animal (vía __index)
local rex = Perro.new("Rex", "Labrador")
print(rex:hablar()) -- "Rex: ¡Guau guau!"La herencia es una cadena de __index: Perro.__index = Perro y la metatable de Perro tiene __index = Animal. Lookup: rex → Perro → Animal. El override es simple: definir el método en Perro esconde el de Animal. Para llamar a la "superclase", usa Animal.hablar(self) explícitamente — no hay super nativo.
Metamethods aritméticos
-- Vector con metamethods:
local Vector = {}
Vector.__index = Vector
function Vector.new(x, y)
return setmetatable({x = x, y = y}, Vector)
end
function Vector.__add(a, b)
return Vector.new(a.x + b.x, a.y + b.y)
end
function Vector.__mul(v, escalar)
return Vector.new(v.x * escalar, v.y * escalar)
end
function Vector.__unm(v)
return Vector.new(-v.x, -v.y)
end
function Vector.__eq(a, b)
return a.x == b.x and a.y == b.y
end
local v = Vector.new(1, 2) + Vector.new(3, 4)
print(v.x, v.y) -- 4, 6Los metamethods aritméticos (__add, __sub, __mul, __div, __mod, __pow, __unm) permiten que los objetos se comporten como tipos nativos. __eq, __lt, __le para comparaciones. Esenciales para tipos matemáticos (Vector, Matrix, Complex) y DSLs transparentes.
Encapsulamiento con closures
-- Estado privado con closures:
local function nueva_cuenta(saldo_inicial)
local saldo = saldo_inicial -- ¡privado!
local historial = {} -- ¡privado!
return {
depositar = function(v)
saldo = saldo + v
historial[#historial + 1] = "+" .. v
end,
retirar = function(v)
if v > saldo then
error("Saldo insuficiente")
end
saldo = saldo - v
end,
get_saldo = function()
return saldo
end,
}
end
local cuenta = nueva_cuenta(100)
cuenta.depositar(50)
print(cuenta.get_saldo()) -- 150
-- cuenta.saldo -- ¡nil! (inaccesible)Lua no tiene private/protected — el encapsulamiento se consigue con closures: las variables locales son capturadas por las funciones devueltas pero inaccesibles externamente. Este patrón "object as closure" es más seguro que las metatables (donde los campos siempre son accesibles vía rawget). Desventaja: más memoria por objeto.
__tostring y __call
-- __tostring (usado por print):
local Point = {}
Point.__index = Point
function Point.new(x, y)
return setmetatable({x = x, y = y}, Point)
end
function Point.__tostring(p)
return string.format("(%g, %g)", p.x, p.y)
end
local p = Point.new(3, 4)
print(p) -- "(3, 4)"
-- __call (objeto como función):
function Point.__call(p, factor)
return Point.new(p.x * factor, p.y * factor)
end
local p2 = p(2) -- ¡se llama como función!
print(p2) -- "(6, 8)"
-- __len (operador #):
function Point.__len(p)
return math.sqrt(p.x^2 + p.y^2)
end
print(#p) -- 5.0 (magnitud)__tostring es invocado por print() y tostring() — permite una representación legible de objetos. __call permite usar objetos como funciones (obj()) — útil para builders y DSLs. __len intercepta el operador #. __concat intercepta ... Estos metamethods hacen los objetos transparentes.
__call como constructor
-- Clase como callable:
local Point = {}
Point.__index = Point
-- Metatable de la CLASE (¡no del objeto!):
setmetatable(Point, {
__call = function(cls, x, y)
return setmetatable({x = x, y = y}, cls)
end
})
-- Point() funciona como constructor:
local p = Point(3, 4) -- ¡sin .new()!
print(p.x, p.y) -- 3, 4
-- Con validación:
local Email = {}
Email.__index = Email
setmetatable(Email, {
__call = function(cls, addr)
assert(addr:match("@"), "Email inválido")
return setmetatable({addr = addr}, cls)
end
})
local e = Email("ana@mail.com") -- OK__call en la metatable de la CLASE permite usar la clase como función — Point(3, 4) en vez de Point.new(3, 4). El primer argumento es la propia clase (cls), seguido de los args. Crea APIs más limpias e idiomáticas. Es el patrón preferido en bibliotecas modernas de Lua. Combinado con validación, el constructor se vuelve seguro.