← Volver al listado de tecnologías

Capítulo 1: El Modelo de Lua - Tablas por Todos Lados

Por: Artiko
luadata-modeltablesmetatablasfundamentos

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:

  1. 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é?”)

  2. 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:

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:

Operadores de comparación:

Operaciones de tabla:

Llamada y conversión:

Gestión de recursos:

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:

  1. Usa tablas, la estructura de datos fundamental de Lua
  2. Implementa metamétodos, el protocolo del Modelo de Lua

Como beneficio, se comporta como una tabla estándar de Lua:

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. __index por 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:

  1. No podemos barajar el mazo (shuffle). Aunque podríamos escribir una función para ello.
  2. 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:

Los metamétodos más importantes son:

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


Próximo: Capítulo 2: Tablas - Arrays y Secuencias