Capítulo 1: El Modelo de Lua - Tablas por Todos Lados
Capítulo 1: El Modelo de Lua
“La simplicidad es la máxima sofisticación.” — Leonardo da Vinci
Una de las mejores cualidades de Lua es su consistencia. Después de trabajar con Lua un tiempo, puedes hacer predicciones informadas sobre características que son nuevas para ti.
Sin embargo, si aprendiste otro lenguaje orientado a objetos antes de Lua, puede parecer extraño usar #t para obtener la longitud de una tabla en lugar de t.length() o t.size(). Esta aparente rareza es la punta de un iceberg que, cuando se entiende apropiadamente, es la clave de todo lo que llamamos “Lua-nic”. El iceberg se llama el Modelo de Lua, y es la API que usamos para hacer que nuestros objetos funcionen bien con las características más idiomáticas del lenguaje.
Puedes pensar en el modelo de datos como una descripción de Lua como un framework. Formaliza las interfaces de los bloques de construcción del lenguaje: secuencias, funciones, iteradores, coroutines, tablas, módulos, y más.
Un Mazo Pythónico… pero en Lua
En lugar de explicar teoría abstracta, veamos un ejemplo práctico. Vamos a crear una baraja de cartas en Lua, aprovechando el modelo del lenguaje.
Primero, necesitamos representar cartas individuales. En Lua, no necesitamos una clase completa para esto:
-- Crear una carta como tabla simple
local function Card(rank, suit)
return {rank = rank, suit = suit}
end
-- Uso
-- >>> beer_card = Card('7', 'diamantes')
-- >>> print(beer_card.rank, beer_card.suit)
-- 7 diamantes
Pero podemos hacer mejor. Vamos a darle un método __tostring para que se vea bien cuando la imprimamos:
local Card = {}
Card.__index = Card
function Card.new(rank, suit)
local self = setmetatable({}, Card)
self.rank = rank
self.suit = suit
return self
end
function Card:__tostring()
return string.format("Card(rank='%s', suit='%s')", self.rank, self.suit)
end
-- Ahora, cuando imprimimos:
-- >>> beer_card = Card.new('7', 'diamantes')
-- >>> print(beer_card)
-- Card(rank='7', suit='diamantes')
La Baraja Francesa
Ahora el punto de este ejemplo: la clase FrenchDeck. Es corta, pero poderosa:
local FrenchDeck = {}
FrenchDeck.__index = FrenchDeck
-- Definir rangos y palos
local ranks = {'2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'}
local suits = {'espadas', 'diamantes', 'tréboles', 'corazones'}
function FrenchDeck.new()
local self = setmetatable({}, FrenchDeck)
self._cards = {}
for _, suit in ipairs(suits) do
for _, rank in ipairs(ranks) do
table.insert(self._cards, Card.new(rank, suit))
end
end
return self
end
function FrenchDeck:__len()
return #self._cards
end
function FrenchDeck:__index(key)
-- Si es un método de la clase, devolverlo
if FrenchDeck[key] then
return FrenchDeck[key]
end
-- Si es un índice numérico, devolver la carta
if type(key) == 'number' then
return self._cards[key]
end
end
Aunque es corta, tiene un gran impacto. Primero, como cualquier colección estándar de Lua, una baraja responde al operador #:
-- >>> deck = FrenchDeck.new()
-- >>> print(#deck)
-- 52
Leer cartas específicas del mazo —digamos, la primera o la última— es fácil, gracias al método __index:
-- >>> print(deck[1])
-- Card(rank='2', suit='espadas')
-- >>> print(deck[52])
-- Card(rank='A', suit='corazones')
¿Deberíamos crear un método para elegir una carta al azar?
No es necesario. Lua ya tiene una forma de obtener un elemento aleatorio de una secuencia. Simplemente usamos una tabla como array:
-- >>> math.randomseed(os.time())
-- >>> local random_card = deck[math.random(#deck)]
-- >>> print(random_card)
-- Card(rank='3', suit='corazones')
Acabamos de ver dos ventajas de usar métodos especiales para aprovechar el Modelo de Lua:
-
Los usuarios de tu clase no tienen que memorizar nombres de métodos arbitrarios para operaciones estándar. (“¿Cómo obtengo el número de elementos? ¿Es
.size(),.length(), o qué?”) -
Es más fácil beneficiarse de la rica biblioteca estándar de Lua y evitar reinventar la rueda.
Pero se pone mejor.
Slicing y Iteración
Como nuestro __index delega al array self._cards, nuestro mazo automáticamente soporta slicing (aunque necesitamos una función auxiliar):
-- Función auxiliar para slicing
local function slice(tbl, first, last, step)
local sliced = {}
step = step or 1
for i = first or 1, last or #tbl, step do
table.insert(sliced, tbl[i])
end
return sliced
end
-- Veamos las 3 primeras cartas
-- >>> top_three = slice(deck._cards, 1, 3)
-- >>> for _, card in ipairs(top_three) do print(card) end
-- Card(rank='2', suit='espadas')
-- Card(rank='3', suit='espadas')
-- Card(rank='4', suit='espadas')
-- Solo los Ases, saltando 13 cartas
-- >>> aces = slice(deck._cards, 13, #deck._cards, 13)
-- >>> for _, card in ipairs(aces) do print(card) end
-- Card(rank='A', suit='espadas')
-- Card(rank='A', suit='diamantes')
-- Card(rank='A', suit='tréboles')
-- Card(rank='A', suit='corazones')
Nuestra baraja también es iterable:
-- >>> for _, card in ipairs(deck._cards) do
-- >>> print(card)
-- >>> end
-- Card(rank='2', suit='espadas')
-- Card(rank='3', suit='espadas')
-- ... (49 cartas más)
También podemos iterar en reversa:
-- >>> for i = #deck._cards, 1, -1 do
-- >>> print(deck._cards[i])
-- >>> end
-- Card(rank='A', suit='corazones')
-- Card(rank='K', suit='corazones')
-- ... (cartas en orden inverso)
Operador in y Búsqueda
La iteración es implícita. Si una colección no tiene un método __contains__ (Lua no lo tiene nativamente, pero podemos simularlo), el operador in hace una búsqueda secuencial:
-- Agregar método de búsqueda
function FrenchDeck:contains(card)
for _, c in ipairs(self._cards) do
if c.rank == card.rank and c.suit == card.suit then
return true
end
end
return false
end
-- >>> print(deck:contains(Card.new('Q', 'corazones')))
-- true
-- >>> print(deck:contains(Card.new('7', 'bestias')))
-- false
Ordenamiento
Un uso común de los metamétodos es emular operadores numéricos. Vamos a rankear cartas por este sistema:
- Los rangos son valores del 2 al 10, con J=11, Q=12, K=13, A=14
- Los palos tienen valores en el orden: espadas (mayor), corazones, diamantes, tréboles (menor)
Aquí hay una función que rankea cartas según esta regla:
local suit_values = {
espadas = 3,
corazones = 2,
diamantes = 1,
tréboles = 0
}
local function card_value(card)
local rank_value
if card.rank == 'A' then
rank_value = 14
elseif card.rank == 'K' then
rank_value = 13
elseif card.rank == 'Q' then
rank_value = 12
elseif card.rank == 'J' then
rank_value = 11
else
rank_value = tonumber(card.rank)
end
return rank_value * 4 + suit_values[card.suit]
end
Ahora podemos ordenar el mazo:
-- >>> table.sort(deck._cards, function(a, b)
-- >>> return card_value(a) < card_value(b)
-- >>> end)
-- >>> for _, card in ipairs(deck._cards) do
-- >>> print(card)
-- >>> end
-- Card(rank='2', suit='tréboles')
-- Card(rank='2', suit='diamantes')
-- Card(rank='2', suit='corazones')
-- Card(rank='2', suit='espadas')
-- ... (en orden de menor a mayor)
-- Card(rank='A', suit='espadas')
Cómo se Usan los Métodos Especiales
Lo primero que debes saber sobre los metamétodos es que están destinados a ser llamados por el intérprete de Lua, no por ti. No escribes deck:__len()(). Escribes #deck, y Lua llama al método __len__ que implementaste.
Para tipos built-in de Lua (tables, strings, functions), el intérprete toma atajos: el operador # para tables obtiene directamente la longitud sin llamar a un método. Pero para objetos definidos por el usuario, Lua busca el método __len en la metatabla.
Más a menudo, la llamada a métodos especiales es implícita. Por ejemplo, la expresión a + b hace que Lua busque __add en las metatablas de a o b.
Cuándo Usar Métodos Especiales
Los metamétodos no son arbitrarios. El Modelo de Lua define qué metamétodos están disponibles y cuándo Lua los invoca. Aquí hay una lista parcial:
Operadores aritméticos:
__add,__sub,__mul,__div,__mod,__pow,__unm(menos unario)
Operadores de comparación:
__eq(igualdad),__lt(menor que),__le(menor o igual)
Operaciones de tabla:
__index(acceso a elemento),__newindex(asignación de elemento)__len(operador#)__pairs,__ipairs(iteración)
Llamada y conversión:
__call(tabla como función)__tostring(conversión a string)__concat(operador de concatenación..)
Gestión de recursos:
__gc(garbage collection)__close(Lua 5.4+, to-be-closed variables)
Por Qué #t Funciona
En lenguajes como Python, llamarías len(my_object) y Python convertiría eso en my_object.__len__(). En Lua, escribes #my_table y Lua busca el método __len en la metatabla.
Esta es una elección de diseño. Roberto Ierusalimschy (creador de Lua) prefirió sintaxis simple sobre métodos. El operador # es más limpio que t:length().
Algunos programadores se quejan de que esto “no es orientado a objetos”. Pero esa crítica pierde el punto. El Modelo de Lua es muy orientado a objetos, solo que no en el sentido clásico Java/C++. Es orientado a prototipos, similar a JavaScript, pero más elegante.
Lo importante es que el patrón es consistente: Lua usa operadores simples, y tú implementas metamétodos para que tus objetos soporten esos operadores.
Un Mazo Lua-nic
Nuestro FrenchDeck es poderoso por dos razones:
- Usa tablas, la estructura de datos fundamental de Lua
- Implementa metamétodos, el protocolo del Modelo de Lua
Como beneficio, se comporta como una tabla estándar de Lua:
- Responde a
#deck - Soporta indexación:
deck[1] - Es iterable con
ipairs - Puede ordenarse con
table.sort
Todo esto sin escribir mucho código. Solo implementamos dos metamétodos: __len y __index.
SOAPBOX: Lua vs Python
Python tiene
__len__,__getitem__,__iter__, y más. Lua tiene menos metamétodos pero son más potentes.__indexpor sí solo puede hacer el trabajo de__getitem__,__getattr__, y más. Esta es la filosofía de Lua: menos características, más poder.
Lo Que No Cubrimos
Nuestro FrenchDeck tiene dos limitaciones:
- No podemos barajar el mazo (shuffle). Aunque podríamos escribir una función para ello.
- No es una “clase” en el sentido clásico. Es una tabla con metatabla.
Hablaremos de cómo barajar en el Capítulo 13, cuando cubramos __newindex. Y la naturaleza de las “clases” en Lua será el tema completo de la Parte III.
Por ahora, lo importante es que entiendas la filosofía: en Lua, todo gira alrededor de tablas y metatablas. Una vez que domines esto, dominarás Lua.
Resumen del Capítulo
El Modelo de Lua es la piedra angular del lenguaje. Al implementar metamétodos, tus objetos pueden:
- Comportarse como colecciones built-in
- Soportar operadores aritméticos y de comparación
- Ser llamados como funciones
- Tener representaciones legibles como strings
- Interactuar con el garbage collector
Los metamétodos más importantes son:
__index: Control total sobre acceso a elementos y atributos__newindex: Control sobre asignaciones__len: Soportar el operador#__tostring: Conversión a string__call: Hacer tablas llamables como funciones
El patrón es siempre el mismo: Lua usa sintaxis simple (operadores, #, etc.), y tú implementas metamétodos para que tus objetos soporten esa sintaxis.
En los siguientes capítulos, profundizaremos en cada aspecto del Modelo de Lua. Pero esta introducción te da el mapa completo.
Lecturas Adicionales
- Programming in Lua, 4th edition - Capítulo 13: Metatables and Metamethods
- Lua 5.4 Reference Manual - Section 2.4: Metatables and Metamethods
- Lua Users Wiki: Metatables Tutorial