Los dos puntos y self
El azúcar sintáctico de la declaración y el de la llamada, qué inserta exactamente el operador de dos puntos, cuántas veces evalúa el receptor, cómo se compila a un solo opcode y el error clásico de mezclar el punto con los dos puntos.
Lua no tiene objetos. Tiene tablas, funciones de primera clase y una regla de reescritura sintáctica que el manual despacha en dos líneas. Todo el sistema de objetos que construirás en este nivel descansa sobre un único operador —los dos puntos— que no hace nada mágico: inserta un argumento. Entender con precisión quirúrgica qué inserta, dónde lo inserta y cuántas veces evalúa el receptor es la diferencia entre escribir código orientado a objetos con criterio y arrastrar durante meses un desplazamiento silencioso de argumentos que solo se manifiesta en el peor momento.
- Desmontar las dos reescrituras distintas que introduce el operador de dos puntos: la de la declaración y la de la llamada.
- Precisar la semántica de evaluación del receptor y por qué no equivale a escribir la expresión dos veces.
- Diagnosticar por su síntoma —no por adivinación— el error de mezclar el punto con los dos puntos.
- Reconocer el coste real en bytecode y entender por qué
selfno es una palabra reservada del lenguaje.
Dos azúcares distintos que comparten símbolo
El primer malentendido consiste en creer que los dos puntos son una característica. Son dos, independientes, que actúan en lugares distintos del programa y que casualmente se escriben igual.
El azúcar de la declaración añade un parámetro implícito llamado self al principio de la lista de parámetros:
local Cuenta = {}
-- Forma azucarada
function Cuenta:depositar(monto)
self.saldo = self.saldo + monto
return self.saldo
end
-- Exactamente el mismo código tras desazucarar
function Cuenta.depositar(self, monto)
self.saldo = self.saldo + monto
return self.saldo
end
El azúcar de la llamada añade un argumento implícito: la propia expresión que precede a los dos puntos.
local c = setmetatable({ saldo = 0 }, { __index = Cuenta })
c:depositar(100) -- forma azucarada
c.depositar(c, 100) -- forma desazucarada; idéntico resultado
Son simétricos por diseño, pero nada obliga a usarlos en pareja. Puedes declarar con punto y llamar con dos puntos, o al revés, y el compilador no protestará: el desajuste solo se manifiesta en tiempo de ejecución.
flowchart LR A[Escribes obj dos puntos metodo arg] --> B[El parser reescribe] B --> C[obj punto metodo con obj como primer argumento] C --> D[La funcion lo recibe en el parametro self] D --> E[self es una variable normal, no una palabra magica]
El receptor se evalúa exactamente una vez
Aquí está la única diferencia semántica real entre las dos formas, y es la razón por la que el azúcar no es puramente cosmético. En expr:metodo(args), la expresión expr se evalúa una sola vez, aunque su valor se necesite dos veces: como tabla donde buscar el método y como primer argumento.
local n = 0
local function siguiente()
n = n + 1
return pool[n]
end
siguiente():activar() -- una sola llamada a siguiente
siguiente().activar(siguiente()) -- dos llamadas: objetos distintos
La segunda línea es un desastre sutil: busca el método en un objeto y se lo aplica a otro. Por eso el manual define el azúcar en términos de una variable temporal invisible, no de sustitución textual.
El mismo razonamiento se aplica a cualquier expresión con efectos observables en la posición del receptor: una indexación sobre una tabla con metamétodo __index de tipo función, una llamada que consume un iterador, un acceso a un proxy que registra lecturas. En todos esos casos la forma con punto duplica el efecto y la forma con dos puntos no.
local accesos = 0
local registro = setmetatable({}, {
__index = function(_, k) accesos = accesos + 1 ; return almacen[k] end
})
registro.pieza:usar() -- accesos sube en 1
registro.pieza.usar(registro.pieza) -- accesos sube en 2
Hay además dos restricciones sintácticas que conviene interiorizar. El nombre que sigue a los dos puntos debe ser un identificador literal —no puedes escribir una indexación calculada con esa sintaxis— y la construcción debe ser una llamada completa: escribir obj:metodo sin argumentos es un error de sintaxis, no una referencia a un método enlazado.
Que Lua no tenga métodos enlazados es una decisión deliberada y no una omisión. Un método enlazado exigiría asignar un objeto nuevo en cada acceso, lo que convertiría una operación de coste cero en una fuente constante de basura, y obligaría a decidir si dos accesos consecutivos al mismo método producen valores iguales. Lua evita el problema entero declarando que un método es una función normal guardada en una tabla, y que enlazarla al receptor es trabajo tuyo, explícito y visible en el punto donde ocurre.
El error clásico: mezclar el punto con los dos puntos
Casi todos los fallos del sistema de objetos de un principiante son la misma equivocación vista desde ángulos distintos. Conviene conocer sus tres caras y, sobre todo, su síntoma característico.
Declarar con dos puntos y llamar con punto
El primer argumento real ocupa la posición de self. Si pasas un número, el error es attempt to index a number value. Si no pasas nada, self vale nil y el mensaje es attempt to index a nil value. El síntoma inconfundible: el error apunta a la primera línea del cuerpo del método.
Declarar con punto y llamar con dos puntos
Sobra un argumento y todos se desplazan una posición. Este es el caso venenoso: si la función ignora los sobrantes y sus parámetros son opcionales, no falla, simplemente hace algo distinto de lo que pediste, en silencio, durante semanas.
Pasar el método como callback
Al escribir boton.onclick = obj.manejar el receptor se pierde: la función viajará sin obj. Como no hay métodos enlazados, la solución es explícita: envolverla en un closure que capture el receptor.
-- Incorrecto: el receptor desaparece por el camino
boton.onclick = obj.manejar
-- Correcto: el closure captura obj y restaura el contrato
boton.onclick = function(...) return obj:manejar(...) end
La regla operativa es breve: los dos puntos se usan si y solo si la función necesita el receptor. Una función de la biblioteca de una clase que no toca self —una fábrica, una utilidad, un validador— debe declararse con punto y llamarse con punto. Mezclar es un error, y la coherencia entre declaración y llamada no la vigila nadie salvo tú.
Coste real y por qué self no es palabra reservada
Existe la superstición de que la forma azucarada es más lenta porque hace dos cosas. Es al revés. El compilador de Lua emite para obj:metodo(x) una única instrucción SELF, que en un solo paso escribe en dos registros consecutivos el resultado de la búsqueda y una copia del receptor. La forma desazucarada obj.metodo(obj, x) necesita una indexación más un movimiento adicional.
-- Comprobación empírica del bytecode; requiere luac
-- luac -l -l archivo.lua muestra SELF frente a GETFIELD mas MOVE
local function con_azucar(o) return o:m(1) end
local function sin_azucar(o) return o.m(o, 1) end
La diferencia es marginal en cualquier programa real, pero desmonta la idea de que el azúcar cuesta algo. Cuesta menos.
El azúcar de la declaración admite además nombres compuestos, lo que permite colgar métodos de tablas anidadas sin ceremonia. Es la forma habitual de organizar una biblioteca por espacios de nombres:
local M = { texto = {} }
function M.texto:recortar(n)
return self.contenido:sub(1, n)
end
-- equivale a: M.texto.recortar = function(self, n) ... end
Queda un último detalle que revela la naturaleza del mecanismo: self no es una palabra reservada. Es un identificador ordinario que la reescritura de la declaración introduce por ti. Puedes escribir function Cuenta.depositar(cuenta, monto) y usar cuenta en todo el cuerpo; el objeto seguirá funcionando idénticamente cuando lo llames con dos puntos. Puedes incluso declarar local self = 3 en el ámbito superior de un archivo. Nada en la máquina virtual sabe qué es un método.
La lección profunda de este nivel es que en Lua no hay invocación de métodos: hay indexación de tablas seguida de llamada a función, y un operador que ahorra teclas. Esto tiene una consecuencia que separa a quien programa Lua de quien traduce mentalmente desde otro lenguaje: como no existe una operación primitiva llamada enviar mensaje, tampoco existe un punto donde el lenguaje pueda equivocarse por ti ni, lo que es más importante, un punto donde pueda protegerte. No hay comprobación de que el receptor sea del tipo esperado, no hay error si un método viaja sin su objeto, no hay diferencia observable entre un campo que guarda un dato y uno que guarda una función. Toda la disciplina que otros lenguajes codifican en su gramática —esta función pertenece a esta clase, este receptor es de este tipo, este método no puede llamarse sin instancia— aquí es un acuerdo entre tú y el que lea tu código dentro de dos años. Por eso la coherencia entre punto y dos puntos no es una cuestión de estilo: es la única barandilla del sistema, y es una barandilla que tú mismo debes sostener en cada línea. Quien interioriza esto deja de buscar el modelo de objetos de Lua y empieza a diseñarlo.
- Escribe una tabla con un método declarado con dos puntos y llámalo con punto. Anota el mensaje de error exacto y en qué línea aparece.
- Repite el experimento al revés: declara con punto, llama con dos puntos, y añade un parámetro opcional para reproducir el fallo silencioso.
- Escribe una función que devuelva un objeto y llámala con
siguiente():metodo(). Instrumenta la función con un contador y confirma que se ejecuta una sola vez. - Ejecuta
luac -l -lsobre las dos formas de invocación y localiza la instrucciónSELFen el listado. - Renombra el parámetro implícito: declara el método con punto y llama al receptor
cuentaen lugar deself. Verifica que la llamada con dos puntos sigue funcionando y explica por qué.