DevTools

Cheatsheet Lua

Linguagem de scripting leve para jogos, embedded e automação

Volver a los lenguajes
Lua
71 tarjetas encontradas
Categorías:
Versiones:

Básico e Tipos


10 cards
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)
end

Lua 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
end

type() 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")
end

En 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


9 cards
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 todos

Los 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)
end

Los 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))  -- 15

Los 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)
end

pcall 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


9 cards
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() end

elseif 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)
end

ipairs 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)
end

Las 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")
end

while 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 end

break 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])
end

Patrones 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
end

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

Las 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


8 cards
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("&lt;&gt;", "&(%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, 2

string.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ígito

Los 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)
end

En 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


8 cards
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)
end

Las 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
end

io.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 usados

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

Lua 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


9 cards
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")  -- 5

Las 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")
end

Los 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º verdadero

and 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


9 cards
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 left

table.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)  -- 10

Lua 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
end

Los 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áticamente

Las 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]
end

Swap-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


9 cards
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)
end

La 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, 6

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