wandres.dev
METATABLAS II · el catálogo completo

Objetos que se llaman, se imprimen y se miden: __call, __tostring y __len

Tres metamétodos que no calculan nada y sin embargo cambian por completo cómo se siente un tipo: el que convierte una tabla en algo invocable, el que le da una representación legible en cualquier traza, y el que le permite definir su propia longitud aunque tenga agujeros.

⏱ 17 min

Los metamétodos aritméticos responden a preguntas de cálculo. Los tres de esta lección responden a preguntas de identidad: qué es esta cosa cuando la invoco, qué es cuando la miro, qué es cuando la mido. Ninguno añade capacidad de cómputo y los tres cambian radicalmente la experiencia de usar un tipo. Con __call una tabla deja de distinguirse de una función; con __tostring tus objetos dejan de aparecer en las trazas como una dirección hexadecimal inútil; con __len el operador de longitud deja de depender de la maldita frontera de la parte secuencia y pasa a significar lo que tú decidas. Son, en la práctica, los tres metamétodos que más se notan.

🎯 Al terminar esta lección sabrás
  • Usar __call para construir valores invocables con estado propio.
  • Dar a un tipo una representación legible con __tostring y conocer sus límites.
  • Definir una longitud propia con __len y saber qué funciones la respetan y cuáles no.
  • Distinguir cuándo estas facilidades ayudan y cuándo esconden lo que ocurre.

Tablas que se comportan como funciones

Cuando Lua intenta llamar a un valor que no es una función, busca __call en su metatabla. Si lo encuentra, lo invoca pasándole el propio valor como primer argumento, seguido de los argumentos originales de la llamada. Es exactamente el mismo desplazamiento que produce la sintaxis de dos puntos en los métodos, pero aplicado a la llamada entera.

local function memoizar(f)
  local cache = {}
  return setmetatable({ aciertos = 0, fallos = 0 }, {
    __call = function(self, x)         -- self es la tabla; x, el argumento real
      local v = cache[x]
      if v == nil then
        self.fallos = self.fallos + 1
        v = f(x)
        cache[x] = v
      else
        self.aciertos = self.aciertos + 1
      end
      return v
    end,
  })
end

local lento = memoizar(function(n) return n * n end)
print(lento(4), lento(4))              -- 16  16
print(lento.aciertos, lento.fallos)    -- 1  1
flowchart TB
A[El programa escribe obj abre parentesis cierra parentesis] --> B[El valor es una funcion de verdad]
B --> C[Si lo es se llama directamente]
B --> D[Si no busca call en su metatabla]
D --> E[Si existe lo llama con el objeto como primer argumento]
E --> F[Detras van los argumentos originales de la llamada]
D --> G[Si no existe lanza attempt to call a table value]

Lo interesante no es el ahorro de cálculo sino la forma: el resultado se usa como una función y se inspecciona como una tabla. Un closure puro guarda su estado en variables locales que nadie de fuera puede leer ni modificar, y esa opacidad es una virtud cuando el estado es un secreto y un estorbo cuando es un dato de interés, como aquí lo son los contadores de aciertos y fallos. Una tabla con __call te da las dos caras del mismo valor: la operación en la línea de llamada y la estructura en la línea de inspección. De ahí que el patrón aparezca en todas partes: constructores que se invocan directamente sobre el módulo, comparadores con configuración, capas de red que son a la vez objeto y operación.

El caso más visible es el del módulo invocable. Una biblioteca de expresiones de coincidencia puede devolver una tabla con funciones auxiliares y, a la vez, dejar que el uso más frecuente se escriba invocando el módulo entero, sin nombrar ninguna función. La tabla sigue siendo el espacio de nombres de siempre; el __call solo añade un atajo para la operación principal, y quien no lo conozca puede ignorarlo por completo.

💡
Un invocable pasa por función casi en todas partes

Cualquier sitio del intérprete que llame a un valor respeta __call: el comparador de table.sort, la función protegida de pcall, el manejador de xpcall, un iterador en un bucle genérico. Las excepciones son las comprobaciones explícitas de tipo: type(obj) sigue devolviendo la cadena table, y una biblioteca que valide con esa comprobación rechazará tu invocable.

Representación y longitud

Los otros dos metamétodos se entienden mejor juntos, sobre un tipo que necesita ambos: una lista que guarda su propio recuento y por tanto admite agujeros.

local Lista = {}
Lista.__index = Lista

function Lista.nueva(...)
  return setmetatable({ n = select("#", ...), ... }, Lista)
end

function Lista:agregar(v)
  self.n = self.n + 1
  self[self.n] = v                     -- v puede ser nil sin romper el recuento
end

function Lista.__len(self)
  return self.n                        -- O de uno, y sin frontera ambigua
end

function Lista.__tostring(self)
  local partes = {}
  for i = 1, self.n do partes[i] = tostring(self[i]) end
  return "Lista[" .. table.concat(partes, ", ") .. "]"
end

local l = Lista.nueva(10, nil, 30)
l:agregar(nil)
print(l)          -- Lista[10, nil, 30, nil]
print(#l)         -- 4
print(rawlen(l))  -- 3 o 1: la frontera cruda, impredecible con agujeros
🖨️

__tostring se usa más de lo que crees

Lo invocan tostring, print, la directiva de cadena de string.format y los mensajes de error cuando el valor lanzado no es una cadena. Debe devolver una cadena: si devuelve otra cosa, el intérprete lanza un error, y si lanza uno él mismo, ese error sale por donde llamaste a print.

🔗

Concatenar no es imprimir

El operador de concatenación no consulta __tostring: consulta __concat. Si quieres que tu objeto se pegue a una cadena, define los dos metamétodos, y haz que __concat llame a tostring sobre ambos operandos.

📏

Quién respeta __len

El operador de longitud y las funciones de la biblioteca de tablas que necesitan el tamaño, como insertar, quitar, concatenar o desempaquetar. rawlen lo ignora por diseño.

🚶

Quién no lo respeta

El iterador de índices numéricos no usa __len: avanza desde uno y se detiene en el primer valor nulo. Con agujeros, recorrer y medir dan resultados distintos, y esa discrepancia es fuente habitual de errores sutiles.

La longitud que devuelva __len debe ser un entero cuando la consume la biblioteca de tablas; un flotante con parte fraccionaria o una cadena provocan un error de longitud no entera. Y conviene recordar que las cadenas ignoran el metamétodo por completo: la longitud de una cadena es siempre su número de bytes, aunque alguien manipule la metatabla compartida de las cadenas.

ℹ️
Por qué una lista con recuento propio no es un capricho

Sin __len, el operador de longitud sobre una tabla con agujeros devuelve cualquier frontera válida: un índice tal que la posición siguiente esté vacía. La implementación usa una búsqueda binaria sobre la parte secuencia, así que el resultado depende del historial de inserciones y puede cambiar entre dos ejecuciones del mismo programa. Guardar el recuento en un campo y exponerlo por __len convierte una operación ambigua y logarítmica en una lectura exacta y constante. Es el caso de uso canónico del metamétodo, y el motivo por el que la función que empaqueta argumentos variables devuelve una tabla con un campo de recuento.

Cuándo no usarlos

Los tres son azúcar, y el azúcar tiene coste. Un __call convierte en invisible el hecho de que hay una tabla con estado detrás de lo que parece una función pura, y quien lea el código en la línea de llamada no tiene forma de saberlo. Un __tostring que trunque o embellezca demasiado puede ocultar precisamente el dato que necesitabas en una traza a las tres de la mañana. Y un __len que no coincida con el recorrido natural del objeto crea dos verdades sobre el mismo valor, que es la peor situación posible.

La regla práctica es sencilla: usa __tostring casi siempre, porque el coste de no tenerlo lo pagas depurando; usa __len cuando tu tipo tenga un tamaño inequívoco que la frontera cruda no puede calcular; y usa __call solo cuando el valor sea conceptualmente una operación y no un contenedor que casualmente sabe hacer algo.

Hay además una consideración de rendimiento que conviene tener presente aunque rara vez decida nada. Los tres metamétodos convierten una operación que el intérprete resuelve en unas pocas instrucciones de su máquina virtual en una llamada a Lua completa, con su marco de pila y su retorno. En un bucle que mida la longitud un millón de veces, la diferencia se nota; en el noventa y nueve por ciento del código, no. La respuesta profesional a esa tensión no es renunciar al metamétodo sino guardar el resultado en una variable local antes del bucle, que es lo que habría que hacer igualmente con una llamada normal.

Estos tres metamétodos no añaden poder, añaden pertenencia

Vale la pena reparar en que ninguno de los tres te permite hacer algo que no pudieras hacer ya. Un invocable equivale a un método al que llamas por su nombre. Una representación legible equivale a una función que convierte tu objeto en texto. Una longitud propia equivale a leer un campo. Si el poder expresivo es idéntico, ¿qué se gana? Se gana que tu tipo deje de ser un ciudadano de segunda dentro del lenguaje. Lua tiene un puñado de protocolos —llamar, imprimir, medir, indexar, iterar— que atraviesan todo el código escrito por todo el mundo, y un valor que participa en ellos puede circular por funciones que jamás oyeron hablar de él, mientras que un valor que no participa obliga a cada consumidor a conocer tu convención particular. Ahí está la asimetría que justifica el esfuerzo: escribir tres metamétodos es un trabajo de minutos que se hace una vez en la definición del tipo, y ahorra una anotación mental en cada uno de los cientos de puntos donde ese tipo se usa. Pero el mismo mecanismo que integra puede disfrazar, y la línea que separa ambas cosas es más nítida de lo que parece: un metamétodo es honesto cuando implementa la operación que el lector espera al ver esa sintaxis, y es deshonesto cuando la aprovecha para hacer otra cosa. Una longitud que cuenta elementos es honesta; una que devuelve la capacidad reservada, no. Un invocable que ejecuta la operación que su nombre anuncia es honesto; uno que además muta estado global, no. El criterio no es ¿puedo? sino ¿qué va a suponer quien lea la línea de llamada, sin abrir mi módulo?.

📝
Lo esencial

__call se dispara al invocar un valor que no es función y recibe ese valor como primer argumento, seguido de los originales; funciona en cualquier punto donde el intérprete llame algo, pero no engaña a una comprobación de tipo. __tostring alimenta la conversión a texto, la impresión y la directiva de cadena del formateador, y debe devolver una cadena, aunque no interviene en la concatenación. __len define la longitud para el operador correspondiente y para la biblioteca de tablas, pero no para las cadenas, ni para la lectura cruda de longitud, ni para el iterador de índices numéricos.

⚔️ Haz que tu tipo participe en los protocolos
  1. Escribe el memoizador y comprueba que type sobre el resultado devuelve la cadena table pese a poder invocarlo.
  2. Pasa un invocable como comparador a table.sort y confirma que funciona. Prueba también con pcall.
  3. Implementa Lista y compara #l, rawlen(l) y el número de vueltas del iterador de índices numéricos sobre una lista con agujeros.
  4. Haz que __tostring devuelva un número en vez de una cadena y lee el error exacto que produce print.
  5. Añade __concat a Lista y explica por qué sin él la concatenación con una cadena falla aunque __tostring esté definido.