La idea: una tabla que describe a otra
Qué es una metatabla y por qué Lua expone el punto de decisión en lugar de la característica: setmetatable y getmetatable, el catálogo completo de eventos, y el mecanismo único que sustituye a media docena de rasgos de otros lenguajes.
Hasta ahora las tablas han sido dóciles: guardas una clave, la recuperas, y si no está te devuelven nil. Ese nil es exactamente el punto donde otros lenguajes insertan media docena de características —clases, propiedades calculadas, sobrecarga de operadores, proxies, finalizadores, referencias débiles— y donde Lua no insertó ninguna. En su lugar dejó un hueco deliberado y una pregunta: ¿y si fueras tú quien decidiera qué ocurre ahí? Una metatabla es la respuesta. No es un tipo nuevo ni una palabra clave: es una tabla corriente cuyos campos, todos con nombres que empiezan por doble guion bajo, describen cómo se comporta otro valor cuando el intérprete se queda sin respuesta obvia.
- Entender la metatabla como descripción del comportamiento de un valor, no como su contenido.
- Manejar
setmetatableygetmetatable, incluida la protección con__metatable. - Situar el catálogo de eventos y saber qué operación dispara cada metamétodo.
- Argumentar por qué un solo mecanismo sustituye a seis características de otros lenguajes.
El hueco deliberado
Toda operación del lenguaje tiene un caso feliz y un caso en que el intérprete no sabe qué hacer. Indexar una tabla con una clave ausente. Sumar dos tablas. Comparar dos valores que no son números ni cadenas. Concatenar algo que no es texto. Convertir un valor a cadena para imprimirlo. En un lenguaje convencional esos casos se resuelven con reglas fijas grabadas en el compilador o con jerarquías de tipos; en Lua se resuelven consultando una tabla que tú controlas.
El nombre lo dice todo: meta tabla, una tabla acerca de otra. La relación es unidireccional y explícita. Un valor no sabe nada de sí mismo; su metatabla es quien contesta por él.
local t = {}
print(getmetatable(t)) --> nil, una tabla nace sin metatabla
print(getmetatable("hola")) --> table: 0x...
print(getmetatable("").__index == string) --> true
Ese segundo resultado es revelador. Las cadenas sí tienen metatabla desde el arranque, y por eso ("hola"):upper() funciona: al indexar la cadena con upper, el intérprete no encuentra nada, va a la metatabla de las cadenas, y su campo __index apunta a la biblioteca string. El método que creías incorporado al tipo es una delegación que puedes leer en el manual.
Hay una asimetría importante que conviene fijar pronto. Las tablas y los userdata tienen metatabla individual: cada instancia puede tener la suya. Los demás tipos —números, cadenas, booleanos, funciones, hilos— tienen metatabla por tipo, una sola compartida por todos los valores del tipo, y desde Lua solo puedes cambiar la de las cadenas si te dejan hacerlo; el resto se fija desde la API C. Esa asimetría no es un capricho: una tabla es un objeto identificable, un número no.
setmetatable y getmetatable
La interfaz completa son dos funciones. setmetatable recibe una tabla y una metatabla —o nil para quitarla— y devuelve la tabla, lo cual permite encadenar la creación en una sola expresión.
local Vector = {}
Vector.__index = Vector
function Vector.nuevo(x, y)
return setmetatable({x = x, y = y}, Vector) -- crea y describe a la vez
end
function Vector.__add(a, b)
return Vector.nuevo(a.x + b.x, a.y + b.y)
end
function Vector.__tostring(v)
return string.format("Vector<%g, %g>", v.x, v.y)
end
function Vector:norma()
return math.sqrt(self.x^2 + self.y^2)
end
local v = Vector.nuevo(3, 4) + Vector.nuevo(1, 1)
print(v, v:norma()) --> Vector<4, 5> 6.4031242374328
En diez líneas hay un tipo con constructor, operador de suma, representación textual y método de instancia. No ha aparecido ninguna palabra clave del lenguaje: solo una tabla llamada Vector que hace de metatabla de sus instancias y, a la vez, de contenedor de sus métodos gracias a la línea Vector.__index = Vector.
getmetatable es la contraparte, con un matiz que casi nadie usa y que conviene conocer. Si la metatabla tiene un campo __metatable, getmetatable devuelve ese valor en lugar de la metatabla real, y setmetatable lanza un error. Es el único mecanismo de encapsulación fuerte del lenguaje.
local opaco = setmetatable({}, {__metatable = "acceso denegado"})
print(getmetatable(opaco)) --> acceso denegado
setmetatable(opaco, {}) --> error: cannot change a protected metatable
Sin __metatable, cualquiera puede leer tu metatabla, copiarla, modificarla o reemplazarla. Con él, el objeto queda sellado. Los sandboxes serios de Lua lo usan sistemáticamente.
Dos hábitos que ahorran disgustos desde el primer día. El primero: la metatabla puede ser compartida por miles de instancias —de hecho debe serlo—, porque es una descripción de comportamiento, no un estado. Crear una metatabla nueva por objeto es el error de novato que multiplica por tres la memoria de un programa.
-- MAL: una metatabla por instancia
local function punto_malo(x, y)
return setmetatable({x = x, y = y}, {__add = suma, __tostring = texto})
end
-- BIEN: una metatabla para todos
local META = {__add = suma, __tostring = texto}
local function punto(x, y)
return setmetatable({x = x, y = y}, META)
end
El segundo: una tabla puede ser a la vez metatabla de unos y valor corriente de otros. Vector del ejemplo anterior es la metatabla de sus instancias, el contenedor de sus métodos y una tabla normal que puedes recorrer con pairs. No hay categorías separadas; el papel de metatabla es una relación, no una propiedad del objeto.
El catálogo de eventos
Un metamétodo se dispara por un evento: una operación concreta que el intérprete no puede resolver sola. La familia es cerrada y cabe en un diagrama.
flowchart LR V[Un valor de Lua] --> M[Su metatabla] M --> A[Acceso con __index __newindex __call __len] M --> B[Aritmetica con __add __sub __mul __div __mod __pow __unm __idiv] M --> C[Bits con __band __bor __bxor __bnot __shl __shr] M --> D[Comparacion con __eq __lt __le] M --> E[Conversion con __concat __tostring __name] M --> F[Ciclo de vida con __gc __close __mode] M --> G[Proteccion con __metatable] style A fill:#a6e3a1,color:#11111b style F fill:#f9e2af,color:#11111b
Tres reglas gobiernan todo el catálogo y explican la mayoría de las sorpresas.
Primera: los metamétodos se buscan en crudo. Cuando el intérprete va a la metatabla a por __add, no dispara a su vez el __index de la metatabla. La búsqueda es directa, sin recursión. Si no lo fuera, el sistema no terminaría nunca.
Segunda: para operaciones binarias basta con que uno de los dos operandos aporte el metamétodo. Lua prueba primero el operando izquierdo y, si no lo tiene, el derecho. Por eso vector + 3 y 3 + vector pueden funcionar los dos con un único __add, siempre que la función tolere recibir los argumentos en cualquier orden.
Tercera: la comparación es más estricta. __eq solo se consulta cuando los dos operandos son tablas —o los dos userdata— y no son el mismo objeto; comparar una tabla con un número siempre da false sin llamar a nadie. Y __lt con __le exigen tipos compatibles, porque el orden entre categorías distintas no significa nada.
Dos eventos merecen mención aparte por su calendario. __gc convierte al objeto en finalizable, pero solo si el campo ya estaba en la metatabla en el momento de llamar a setmetatable: añadirlo después no marca nada. Y __close, incorporado en Lua 5.4, es lo que hace posibles las variables to-be-closed declaradas con el atributo close, la forma que tiene Lua de garantizar liberación determinista de recursos sin un bloque try ni un destructor.
Un mecanismo, media docena de características
Clases y herencia
No hay class ni extends. Hay una tabla que sirve de metatabla, un __index que apunta a los métodos y, para heredar, una metatabla sobre la metatabla. Toda la jerarquía es lectura de campos.
Propiedades calculadas
Lo que en otros lenguajes es un get declarado en la definición del tipo, aquí es un __index implementado como función: la clave no existe, se calcula al vuelo y el llamador no nota la diferencia.
Sobrecarga de operadores
Cada operador tiene su evento. Añadir aritmética, comparación o concatenación a un tipo propio es asignar un campo, no modificar la gramática del lenguaje.
Proxies e inmutabilidad
Con __index y __newindex sobre una tabla vacía interceptas cada lectura y cada escritura: registros de acceso, validación, solo lectura, copia perezosa. Es la lección 10.3 entera.
Finalizadores y recursos
__gc y __close dan lo que en otros lenguajes son destructores y bloques con recurso: cierre determinista de ficheros, sockets o handles nativos sin sintaxis dedicada.
Referencias débiles
El campo __mode le dice al recolector que las claves o los valores de esa tabla no cuentan como referencias vivas. Cachés y tablas asociativas sin fugas, con una cadena de dos caracteres.
Lo verdaderamente radical de las metatablas no es que permitan sobrecargar operadores; casi cualquier lenguaje lo permite de una forma u otra. Lo radical es la unidad de extensión que eligieron. Un lenguaje convencional te ofrece características: clases, interfaces, propiedades, operadores, destructores, delegación, cada una con su sintaxis, sus reglas de visibilidad y sus interacciones sutiles con las demás. Lua no te ofrece ninguna: te ofrece la lista de los momentos exactos en que el intérprete se queda sin saber qué hacer, y un sitio donde apuntar tu respuesta. La consecuencia es que las características no son atómicas sino compuestas, y por tanto negociables. Puedes construir herencia simple, múltiple, por copia, por delegación o mixta, porque la herencia no está en el lenguaje: está en cómo encadenas búsquedas fallidas. Puedes construir objetos inmutables, objetos que registran cada acceso, objetos que existen solo cuando alguien los pide, objetos cuyos campos viven en otro proceso, y ninguno requiere permiso del diseñador del lenguaje. El precio es real y hay que mirarlo de frente: cuando la abstracción no está en el lenguaje, tampoco está en las herramientas, y ningún analizador estático va a entender del todo un __index que consulta la red. Ese es el intercambio de Lua, y explica por qué el mismo mecanismo que hace el lenguaje diminuto lo hace, también, ilimitado.
- Crea una tabla vacía y comprueba con
getmetatableque no tiene metatabla; asígnale una consetmetatabley vuelve a comprobarlo. - Verifica en tu intérprete que
getmetatable("").__indexes la bibliotecastring, y explica en una frase por qué funciona("abc"):rep(2). - Escribe un tipo
Fraccioncon__addy__tostring. Haz queFraccion.nuevo(1,2) + Fraccion.nuevo(1,3)imprima el resultado ya simplificado. - Sella un objeto con
__metatablee intenta cambiar su metatabla dentro de unpcall: lee el mensaje de error exacto que devuelve. - Investiga: crea una tabla con
__gcen la metatabla y otra a la que se lo añades después desetmetatable. Fuerza uncollectgarbagey observa cuál de las dos se finaliza.