La llamada: paso de argumentos, azúcares y dos puntos
Llamar a una función parece la operación más trivial del lenguaje y es, en Lua, la que más malentendidos acumula. Esta lección reconstruye el modelo exacto de paso de argumentos y demuestra que Lua pasa siempre por valor, incluidos los tipos que la gente llama por referencia, porque lo que se copia en esos casos es la referencia y no la estructura apuntada, distinción que decide qué modificaciones se ven fuera y cuáles no. Después se estudia el ajuste de aridad, es decir, qué ocurre cuando faltan argumentos y qué ocurre cuando sobran, y por qué esa tolerancia silenciosa es a la vez el idioma canónico de los parámetros opcionales y una fuente real de errores mudos. Se examinan a continuación las dos abreviaturas de llamada que Lua reserva para el argumento único, la tabla literal y la cadena literal, y el papel que juegan en las bibliotecas con configuración declarativa. La lección cierra con el azúcar de los dos puntos, que no es orientación a objetos sino la inserción automática de un primer argumento, y con la lectura de por qué esa reducción del método a una convención de llamada es lo que permite que el sistema de objetos de Lua no exista como característica del lenguaje.
Hay una frase que circula por foros y tutoriales y que conviene desactivar antes de seguir: la de que en Lua los números y las cadenas se pasan por valor y las tablas por referencia. Es falsa, y su falsedad no es una pedantería terminológica sino la raíz de la mitad de las confusiones sobre qué modificaciones sobreviven a una llamada. Lua pasa absolutamente todo por valor, sin una sola excepción. Lo que ocurre es que algunos de sus valores son referencias a estructuras que viven en otro sitio, y copiar una referencia es copiar una dirección, no lo que hay al final de ella. Con ese único ajuste mental se explican de golpe todos los comportamientos que parecían inconsistentes: por qué reasignar un parámetro nunca se ve desde fuera, por qué modificar un campo sí, y por qué las funciones que pretenden vaciar una tabla tienen que borrar sus claves en lugar de asignarle una tabla nueva. Esta lección construye ese modelo y después recorre las tres piezas de azúcar sintáctico que rodean a la llamada, incluida la más famosa de todas, los dos puntos.
- Explicar con precisión que Lua pasa todo por valor y qué se copia exactamente cuando el valor es una tabla o una función.
- Predecir el comportamiento de las llamadas con argumentos de menos y de más, y usar esa tolerancia deliberadamente.
- Emplear las abreviaturas de llamada con tabla literal y con cadena literal, y saber cuándo aportan y cuándo estorban.
- Reescribir cualquier llamada con dos puntos como la llamada con punto equivalente, en ambas direcciones.
Paso por valor, incluso cuando el valor es una referencia
Los ocho tipos de Lua se dividen en dos grupos según lo que la variable guarda. Números, booleanos, cadenas y nil guardan el dato en sí. Tablas, funciones, hilos y userdata guardan una referencia a un objeto del montón. En ambos casos, llamar a una función copia lo que la variable guarda en el parámetro correspondiente. La consecuencia es doble y muy limpia.
local function reasignar(t)
t = { 99 } -- solo cambia la COPIA local de la referencia
end
local function mutar(t)
t[1] = 99 -- sigue la referencia y modifica el objeto compartido
end
local datos = { 1 }
reasignar(datos) print(datos[1]) --> 1 nada cambio fuera
mutar(datos) print(datos[1]) --> 99 el objeto era el mismo
-- Vaciar una tabla desde dentro exige tocar el objeto, no la variable
local function vaciar(t)
for k in pairs(t) do t[k] = nil end
end
-- Las cadenas son inmutables: no hay mutacion posible, solo cadenas nuevas
local function gritar(s)
s = s:upper() -- crea otra cadena y la guarda en el parametro
return s
end
El parámetro es, a todos los efectos, una variable local más de la función, inicializada con el valor del argumento. Asignarle algo nuevo es cambiar esa local y nada más. Seguir la referencia y escribir en el objeto apuntado es otra operación distinta, y es la única que el llamante puede observar.
Aridad: lo que falta y lo que sobra
Lua no comprueba el número de argumentos. Si pasas menos de los declarados, los parámetros restantes valen nil; si pasas más, los sobrantes se descartan sin traza. Este es el mismo ajuste de listas que gobierna los retornos múltiples de la lección anterior, aplicado a la lista de argumentos.
local function saludar(nombre, saludo, signo)
saludo = saludo or "Hola" -- idioma canonico de valor por defecto
signo = signo or "!"
return saludo .. ", " .. nombre .. signo
end
print(saludar("Ada")) --> Hola, Ada!
print(saludar("Ada", "Buenas")) --> Buenas, Ada!
print(saludar("Ada", "Ey", "?", 1, 2)) --> Ey, Ada? los extras se tiran
-- Cuidado: el idioma con or no distingue nil de false
local function activar(flag)
if flag == nil then flag = true end -- forma correcta si false es valido
return flag
end
-- Y un caso clasico de error mudo: coma de mas, argumento desplazado
-- table.insert(t, 3, valor) inserta EN LA POSICION 3
-- table.insert(t, valor) inserta AL FINAL
La tolerancia es cómoda y tiene un coste real: una llamada con los argumentos en orden equivocado, o con uno de menos por un descuido, no falla en el punto de la llamada sino mucho después, cuando alguien intenta usar un nil inesperado. Es el precio de no tener firmas verificadas, y la razón de que las bibliotecas maduras validen sus entradas explícitamente en las funciones públicas.
La convención que mejor funciona en Lua es asimétrica: las funciones que forman la interfaz pública de un módulo comprueban tipos y presencia de sus argumentos y fallan con un mensaje claro, mientras que las funciones internas confían en quien las llama. Así el coste de la validación se paga una vez en el borde en lugar de en cada capa, y el error aparece donde el usuario puede entenderlo. La función de aserción de tipo con nombre del argumento y número de posición es una de las primeras utilidades que aparece en casi todo proyecto serio.
Los tres azúcares: tabla, cadena y dos puntos
Lua reserva dos abreviaturas para el caso de un único argumento literal, y una tercera para insertar un primer argumento implícito. Las tres son reescrituras puramente sintácticas, sin ninguna semántica propia.
-- Argumento unico que es una tabla literal: los parentesis sobran
crear{ nombre = "mapa", ancho = 64, alto = 64 }
-- equivale a: crear({ nombre = "mapa", ancho = 64, alto = 64 })
-- Argumento unico que es una cadena literal: idem
require "socket" -- equivale a require("socket")
print "hola" -- equivale a print("hola")
local re = ("%d+"):rep(2) -- el parentesis es para el literal, no la llamada
-- Dos puntos: inserta el receptor como PRIMER argumento
local cuenta = { saldo = 100 }
function cuenta.ingresar(self, cantidad) self.saldo = self.saldo + cantidad end
cuenta:ingresar(50) -- equivale a cuenta.ingresar(cuenta, 50)
-- Y en la declaracion, los dos puntos declaran self implicitamente
function cuenta:retirar(cantidad) self.saldo = self.saldo - cantidad end
-- equivale a: function cuenta.retirar(self, cantidad) ... end
-- La trampa clasica: mezclar las dos formas
local metodo = cuenta.ingresar
metodo(cuenta, 10) -- correcto: self va explicito
-- metodo(10) -- error: self valdria 10
La abreviatura de tabla literal es la que sostiene el estilo declarativo de innumerables bibliotecas de Lua, desde definiciones de configuración hasta constructores de interfaces, porque permite que una llamada a función parezca un bloque de datos. La de cadena literal es la razón de que require se escriba sin paréntesis en la mayoría del código que verás.
Siempre por valor
Se copia lo que la variable guarda. Si guarda una referencia, se copia la referencia; el objeto apuntado sigue siendo uno solo.
Aridad tolerante
Faltan y valen nil, sobran y se descartan. Nunca hay error de aridad, y por eso el error aparece tarde.
Un solo argumento literal
Con una tabla o una cadena literal como único argumento, los paréntesis son opcionales. Es lo que hace declarativo el estilo de muchas bibliotecas.
Dos puntos igual a primer argumento
No hay métodos ni receptores en la semántica. Hay una convención de llamada que inserta un argumento llamado self.
flowchart LR A[obj dos puntos metodo con args] --> B[obj punto metodo con obj y args] B --> C[Busqueda de la clave metodo en obj] C --> D[Llamada normal con self como primer parametro] E[Llamada con tabla literal] --> F[Llamada normal de un argumento] style B fill:#a6e3a1,color:#11111b style D fill:#89b4fa,color:#11111b
El azúcar de los dos puntos suele presentarse como el soporte de Lua para la orientación a objetos, y esa descripción invierte exactamente lo que ocurre. Lua no soporta objetos: soporta insertar un argumento. Todo lo que en otros lenguajes es maquinaria dedicada aquí se descompone en dos operaciones que la tabla ya sabía hacer y en una reescritura sintáctica de tres caracteres. La búsqueda del método es una búsqueda de clave en una tabla, con el desvío por el metacampo de índice cuando la clave no está, que es lo que llamamos herencia. El receptor es el primer parámetro, con nombre y visible en la firma cuando la escribes con punto. Y el despacho dinámico, que en un lenguaje con clases exige una tabla de métodos virtuales gestionada por el compilador, aquí es simplemente que la clave se busca en tiempo de ejecución sobre el valor que tengas delante. De esa reducción se siguen consecuencias que no son estéticas. La primera es que puedes escribir un sistema de objetos completo, con herencia simple, múltiple, mixins, propiedades calculadas o control de acceso, en unas pocas decenas de líneas que puedes leer entero, y que si no te gusta puedes escribir otro distinto sin salir del lenguaje. La segunda es que no hay una frontera entre datos y objetos: la misma tabla que sirve de configuración sirve de instancia, y por eso serializar, inspeccionar o recorrer un objeto no requiere reflexión, solo un bucle sobre sus claves. La tercera, y la más subestimada, es que el error clásico de invocar un método con punto en vez de dos puntos deja de ser un misterio del entorno de ejecución y pasa a ser algo perfectamente diagnosticable: pasaste un argumento de menos, y todos los demás se corrieron una posición. La cuarta es que un método es un valor como cualquier otro, de modo que guardarlo, envolverlo para medir su tiempo, reemplazarlo en caliente o interceptarlo es asignar a un campo, sin sistema de aspectos, sin proxies del lenguaje y sin permiso de nadie. Ese es el intercambio de Lua en su forma más pura: no te doy un sistema de objetos, te doy el argumento implícito y la búsqueda de clave, y con eso construyes el sistema de objetos que tu problema necesite. Quien busca la palabra clave que falta no encuentra nada. Quien entiende la reescritura descubre que no faltaba.
- Escribe dos funciones, una que reasigne su parámetro de tabla y otra que modifique un campo, y comprueba desde fuera cuál de las dos se nota. Explica el resultado sin usar la palabra referencia como si fuera un modo de paso.
- Implementa una función que vacíe una tabla recibida y demuestra por qué asignarle una tabla nueva dentro no sirve.
- Llama a una función de tres parámetros con uno, con tres y con cinco argumentos. Anota qué pasa en cada caso y por qué no hay ningún error.
- Escribe un valor por defecto con el operador
ory después demuestra que falla cuando el argumento legítimo esfalse. Corrígelo comparando connil. - Diseña una función de configuración pensada para llamarse con una tabla literal sin paréntesis, y otra pensada para una cadena literal. Escribe ambas llamadas en las dos formas.
- Toma un método declarado con dos puntos, extráelo a una variable y llámalo mal. Lee el error, identifica qué valor acabó en
selfy arréglalo de las dos maneras posibles.