« NetLogo Wishlist « || Inicio ||

Lua: La Elegancia de un Buen Diseño

Última modificación: 29 de Mayo de 2020, y ha tenido 150 vistas

Etiquetas utilizadas: || ||

Basado en A Look at the Design of Lua.

En la abundancia actual, donde cada semana nacen cientos de lenguajes de programación y se establece una especie de niebla unificadora en el horizonte informático, pocas veces tiene uno la suerte de encontrarse con un lenguaje que despierte un interés tan profundo como para conocer cómo está hecho y cómo aborda los problemas habituales. Muchas menos, si además esa suerte va acompañada de un lenguaje honesto y que no oculta sus trucos tras capas de barniz.

Hay lenguajes que motivan el descubrimiento y la experimentación (cada cual encuentra los suyos... en mi caso, Logo, Haskell, Scheme, Elm, NetLogo, Julia), y otros que me llevan al hartazgo (como C, C++, Java, Python, R, Javascript,...). Así que la sorpresa de encontrarse con un lenguaje antiguo (ya casi 30 años), pero tan bien construido que a uno le da la sensación de que mucho de lo que ha llegado después ha sido como un retroceso de ideas, deja la sensación de que se abre un nuevo camino que merece la pena ser explorado.

Lua, un lenguaje del que se oye hablar desde hace años, pero que hasta ahora no tuve la ocasión de explorar, ha sido capaz de despertar mi interés por caminos que pensaba cerrados hace tiempo, y más cuando además se descubre que llega desde Brasil y no desde los megacentros habituales.

Ante un lenguaje tan compacto y bien diseñado uno solo puede preguntarse, ¿porqué no ha mantenido el éxito como lenguaje general de script (y podríamos quitar incluso "de script")?, y quizás la respuesta se encuentre más fuera que dentro del propio lenguaje, y se deba a la calidad media de la programación actual, donde cada vez se hace más difícil encontrar artistas-programadores proyectando ideas, y cada vez son más frecuentes los programadores-ensambladores de productos manufacturados por otros. Sin duda, el programador que solo conoce APIs es cada día más común, y parece ser el futuro de los productos digitales y la tan vanagloriada "democratización" de los servicios, que nos lleva a un mundo donde la calidad no importa, sino disponer de piezas de montaje que cualquiera pueda conectar.

¿Quizás su retroceso frente a lenguajes claramente más ineficientes y feos (Python, Javacript... sí, claramente con otros objetivos en mente) se deba a su excesivo éxito en un nicho muy concreto? En este caso, el scripting de video-juegos, una de las áreas más interesantes en las que un programador puede intentar resolver problemas (donde las medias soluciones no son soluciones, y donde bajos rendimientos por una mala programación no tienen cabida), pero a la vez un área que en cierta forma se ve cubierta de cierto desprestigio por parte de "programadores serios" (que hoy en día suelen acabar conectando módulos para crear soluciones web).

Pero no solo el lenguaje ha sido un magnífico descubrimiento, el libro Programming in Lua, escrito por Roberto Ierusalimschy, uno de los creadores del lenguaje, es una de esas joyas de programación que se esconde tras un título neutro. He aprendido de este libro más que de los muchos manuales, blogs, y libros de programación que he leído en muchos años... y ahora ocupa un lugar de honor en mi estantería junto con Structure and Interpretation of Computer Programs de Harold Abelson, Gerald Jay Sussman y Julie Sussman. Creo que colocarlo cerca indicará para muchos lectores algo más que palabras. Sus algo más de 300 páginas fueron consumidas en apenas 2 días de lectura, pero sabiendo que deberé volver a él para llegar a entender muchos conceptos que, pese a elegantes y basados en ideas directas, abren nuevas formas de ver la programación con un lenguaje como Lua.

Aprovecho el artículo A Look at the Design of Lua para dar una versión abreviada y española de las cosas que más me han llamado la atención de este lenguaje.

¿Qué es Lua?

Lua es un lenguaje de script desarrollado en la Universidad Católica Pontificia de Río de Janeiro (Brasil) en 1993 por Roberto Ierusalimschy, Luiz Henrique De Figueiredo y Waldemar Celes. Durante mucho tiempo ha sido la estrella de los lenguajes de script, sobre todo en la generación/modificación de videojuegos (quizás porque sus autores pertenecen al Dpto de Computación Gráfica de esa universidad) aunque, como veremos en el resto de esta entrada, tiene características de peso suficiente como para ser una estrella en muchos más ámbitos (no en balde, se usa también como lenguaje de script en herramientas muy diversas, como puede verse en esta página (https://en.wikipedia.org/wiki/List_of_applications_using_Lua)). La versión 5.3, la versión estable actual, se publicó en 2015, y en un plazo de unas semanas se espera la versión 5.4 (está bien construido, así que no necesita estar sacando versiones cada 3 meses para ser estable y ofrecer soluciones robustas).

A pesar de tener una estructura simple y muy compacta, y encajar dentro de los lenguajes procedurales, está construido de forma que es capaz de adoptar paradigmas adicionales de forma muy natural, por lo que no es extraño referirse a él como un lenguaje funcional, orientado a objetos (sobre estas dos interpretaciones hablaremos algo más en esta entrada), u orientado a datos (ya que ofrece un magnífico soporte a la descripción de datos incluso desde años antes a la aparición de protocolos como XML o lenguajes como Javascript). Su tamaño ligero es una de las esencias de este lenguaje, ya que desde el principio tenía como objetivo ofrecer un pequeño conjunto de mecanismos que permitiesen al programador extenderlo en la dirección que requiriese en vez de ofrecer una recopilación infinita de librerías que intentan hacer de todo pero en una sola dirección. Así, Lua reduce su carga conceptual esencialmente a 3 ideas:

  • el uso de tablas como única estructura de datos,
  • las funciones como elementos de primera clase para permitir la abstracción, y
  • las corrutinas para proporcionar control a diversos niveles de integración y ejecución.

Es el programador el que, encima de estas 3 herramientas, construye el aparato que se encuentra en otros lenguajes, promoviendo el uso de módulos, objetos, entornos de ejecución y datos, etc. El resultado es un lenguaje flexible (quizás demasiado flexible para aquellos acostumbrados a un lenguaje de programación de producción industrial), que brilla con todo su esplendor cuando tiene que interactuar con soluciones existentes y programadas en otros lenguajes... una visión tan abierta, que pocos lenguajes pueden permitirse el lujo de tenerlo en su foco. La falta de herramientas precargadas exige que el programador entienda bien lo que está haciendo, ya que la mayoría de las construcciones se muestran explícitas en el lenguaje, no ocultas tras una API completamente ajena.

Más adelante, en esta misma entrada, intentaremos analizar un poco cómo se puede realizar este tipo de aparatajes de una forma sencilla en Lua, pero antes veamos algo más acerca de las características del lenguaje.

Características de Lua

Intentando ser simple, Lua ofrece tipos dinámicos (algunos complementos actuales del lenguaje lo intentan orientar a un sistema de tipos más estático y fuerte), estructuras de datos dinámicas, un buen motor de recolección de basura, y una función del tipo eval (aunque veremos que con ciertas características muy interesantes).

Además, proporciona soporte para 8 tipos de datos básicos:

  • nil (que es el valor que se usa para indicar que un valor no existe, o que una variable no está asignada),
  • booleano (solo nil y false son falsos, todo lo demás es verdadero),
  • números (sin distinción de tipos),
  • cadenas,
  • userdata (bloques de memoria con acceso por punteros directamente, estructuras de datos de C),
  • tablas,
  • funciones, e
  • hilos (thread, que son los que representan las corrutinas).

Los 5 primeros son habituales en muchos lenguajes, y sobre los tres últimos daremos más información más adelante.

Con estos fundamentos, los objetivos principales que los diseñadores del lenguaje quieren proporcionar a Lua son:

  • Simple. Como hemos comentado, el objetivo es proporcionar unos pocos mecanismos muy potentes que permitan construir herramientas para solucionar problemas de forma adaptable al usuario/programador. Como indicador de este objetivo, el manual de referencia completo de Lua apenas tiene 100 páginas, donde se cubre el lenguaje completo, sus librerías estándar, y la API con C.
  • Ligero. Su implementación completa consta de unas 25,000 líneas de código C, con binarios que rondan los 200Kb.
  • Portable. Lua está implementado en ISO C y corre en cualquier sistema con más de 300Kb de memoria, lo que permite disponer de él en sistemas grandes, como mainframes, o en algo tan pequeño como un microcontrolador.
  • Empotrable. Fue diseñado para interoperar con otros lenguajes, por lo que su distribución principal es en forma de librería con una API con C. Esta librería exporta funciones que crean un nuevo estado de Lua, carga código en ese estado, llama a las funciones cargadas en el mismo estado, accede a las variables globales en el estado, y realiza otras operaciones básicas. El intérprete de Lua (con el que normalmente trabajamos los novatos en este lenguaje) es simplemente una pequeña aplicación construida sobre esta librería.

Vamos a centrarnos en los tres elementos principales que hemos anotado y que proporcionan toda la elegante potencia que este lenguaje puede desplegar:

Tablas

Las tablas en Lua son lo que en otros lenguajes se han llamado listas asociativas, arrays asociativos, diccionarios o maps. Así que es simplemente una colección de pares de la forma (clave,valor). Lo importante es que en Lua es el único mecanismo de estructurar datos que se ofrece y, aunque no es una particularidad de este lenguaje (muchos otros lenguajes lo ofrecen), en Lua las tablas constituyen una de las piedras fundamentales sobre la que se sustentan todos los demás desarrollos potenciales,... y todo gracias a un diseño muy inteligente y muy eficiente. Sobre este tipo de estructuras los programadores han añadido otras habituales, como registros (records), arrays, listas, conjuntos (bags), pilas, colas, matrices, matrices dispersas, etc.

Pero Lua no se para ahí, y las tablas son, como veremos, el soporte del sistema de módulos (como organización conceptual y real del código), de los objetos (ofreciendo un paradigma de programación orientada a objetos), e incluso del sistema de entornos que permite encapsular la ejecución de funciones.

Acceder al valor de una determinada clave, "x", en una tabla, t, es tan sencillo como escribir: t["x"], aunque con acierto y un poco de azúcar sintáctico Lua también permite escribirlo como t.x. Además, proporciona constructores directos, las llaves {..}, como en : { x = 1, y = 2 }, que crea una tabla donde se han asociado los pares ("x",1) e ("y",2). De esta forma, un registro se convierte en un tipo particular de tabla. Cualquier dato de Lua puede actuar tanto como clave como valor en una tabla.

Los arrays no son más que tablas cuyas claves son números enteros consecutivos (comienzan por el índice 1, por suerte para los más matemáticos), e incluso se facilita el trabajo con ellos evitando tener que introducir las claves, ya que su posición determina el índice. Así, por medio del constructor de tablas podemos escribir t = {10, 20, 30}, para definir un array con esos tres elementos, a los que podríamos acceder por medio de t[1], t[2] y t[3].

Es interesante que los arrays muy dispersos (donde una gran cantidad de posiciones está vacía) se pueden conseguir indicando solo las posiciones ocupadas, y Lua no reserva espacio para aquellas no ocupadas, por lo que el ahorro de memoria al trabajar con grandes estructuras de datos de este tipo es considerable.

Funciones

En Lua las funciones anónimas son elementos de primera clase, soportando todo lo que se entendería como $\lambda$-cálculo. De hecho, todas las funciones son anónimas, y una definición del tipo:

function suma (x,y)
    return x + y
end

no es más que una escritura más cómoda de:

suma = function (x,y)
    return x + y
end

Así pues, las funciones se pueden pasar como parámetros de otras funciones, devolver como resultados, o ser clave y valor de una tabla.

Hay más cosas llamativas en el uso de funciones en Lua, pequeños detalles que hacen que más adelante haya cosas que se resuelvan de forma muy sencilla. Por ejemplo, las funciones no tienen un aridad determinada, eso quiere decir que si reciben menos datos de entrada de los que aparecen en su definición, simplemente completa con valores nil, y si recibe más, los sobrantes (por la cola) son ignorados.

Los chunks como funciones anónimas

Como en otros lenguajes (sobre todo, interpretados o con vocación de empotrables), en Lua se usa el término chunk para denotar un bloque de código que se debe ejecutar de forma conjunta (por ejemplo, si usamos un intérprete REPL básico, cada línea de código introducida puede ser un chunk). Esencialmente, debemos entender por chunk como cada unidad que el compilador de Lua compila independientemente, por lo que dispone de un entorno de ejecución independiente de los demás chunks.

Cuando Lua se encuentra con un chunk, no lo evalúa de forma imperativa, como podría parecer por el tipo de lenguaje que es, sino que lo rodea de un entorno de función:

function ()
    ... chunk
end

y devuelve una función anónima evaluable que puede ser ejecutada como cualquier otra función que hayamos definido.

La ventaja es que, de esta forma, todo lo que se haya definido dentro del chunk es local a esa función, por lo que el resultado es un lenguaje funcional puro sin efectos secundarios, donde no existe diferencia entre el concepto de código global y función... Un código será global respecto a una función si la evaluación de esta última se ha producido dentro de la función asociada al código. Puede parecer un detalle menor, pero es algo esencial cuando estamos trabajando con un lenguaje que tiene como objetivo ser empotrable en otros procesos sin producir interferencias de ejecución y efectos indeseables.

Además, veremos que esta decisión a la hora de evaluar y compilar código tendrá efectos muy beneficiosos en otros usos.

Uso combinado de tablas y funciones

Módulos

Cómo se construyen y funcionan los módulos de Lua es un claro ejemplo del uso combinado de tablas y funciones de una forma muy interesante. Comenzar con ellos nos dará un primer acercamiento a ciertas técnicas que después veremos en otros usos para ampliaciones del lenguaje.

En muchos lenguajes, tras cargar una librería/módulo, por ejemplo, la típica librería math que contiene definiciones de funciones matemáticas, podemos hacer un uso cualificado de sus funciones de forma similar a ésta:

math.sin(math.pi/2)

que interpretamos como el uso de la función sin para calcular el seno del ángulo $\pi/2$ ($seno(\pi/2)$). Pero teniendo en cuenta lo que hemos visto de tablas y funciones, ¿no podríamos pensar que math es una tabla en la que encontramos una clave, sin, asociada a la función que calcula el seno, y una clave, pi, para el valor numérico de $\pi$?

Pues eso es exactamente lo que está pasando en Lua. Cuando se carga una librería, realmente se está cargando el código de ésta como si fuera un chunk... el chunk del código que crea la tabla asociada a la librería. Si vemos cómo se definen los módulos en Lua nos encontraremos una estructura como la que sigue (librería para crear números complejos y las operaciones con ellos, extraída del libro de Lua, pero donde he cambiado los comentarios):

local M = {} -- la tabla-módulo
    
-- Función para crear un nuevo número complejo... que también es una tabla
    
local function new (r, i)
    return {r=r, i=i}
end
    
M.new = new -- añade la función 'new' al módulo
    
-- constante 'i' (imaginaria)
    
M.i = new(0, 1)
    
-- Función suma de dos complejos
    
function M.add (c1, c2)
    return new(c1.r + c2.r, c1.i + c2.i)
end
    
-- Otras funciones
    
-- ....
    
return M -- Devolvemos la tabla-módulo

Obsérvese que en este caso el módulo simplemente rellena todo lo que necesitamos para trabajar con números complejos, tanto las constantes (como $i$), como las funciones para manipularlos. Para cargar la librería usamos una función de la librería estándar que simplemente carga el fichero (supongamos que le hemos dado el nombre complex.lua) como un chunk, devuelve la función que se obtiene al compilar el chunk, y ejecuta la función que esta compilación devuelve... por lo que en este caso se devuelve la tabla que contiene constantes y funciones adecuadas para trabajar con números complejos:

local comp = require "complex"

A partir de este momento, podemos escribir comp.new(1,2) para obtener $1+2i$.

Con las herramientas básicas (tablas, funciones, y evaluación de chunks) y una única función adicional (que concatena algunos procesos comunes), require, Lua proporciona un mecanismo muy eficaz para trabajar con módulos/librerías de una forma muy económica y eficiente.

Es muy interesante que Lua usa este sistema de módulos para mantener un tamaño pequeño en sus producciones, permitiendo descomponer las construcciones complejas por medio de los mecanismos en los que se basa. De esta forma, estamos seguros de poder controlar el tamaño de nuestras soluciones al añadir librerías/módulos que amplíen las funcionalidades. El control sobre require es completo.

Entornos

Un programa en Lua es un conjunto de módulos cargados por medio de require dentro de un entorno único (que podríamos considerar el contexto global). Cada uno de esos módulos funcionan con ámbitos léxicos independientes, lo que significa que las variables definidas en cada uno de ellos es invisible para los demás.

Aunque no entraremos en detalles acerca de cómo lo gestiona Lua (se pueden ver las referencias del lenguaje o el libro al que hicimos mención al principio de esta entrada para tener una idea más fiel), es interesante hacer notar que Lua no necesita ninguna extraña y costosa estructura adicional para mantener esta separación de ámbitos, sino que utiliza tablas de entorno adicionales (en cada uno de los ámbitos que va ejecutando), llamadas _ENV, en las que almacena qué variables y valores corresponden a ese ámbito. Esta propiedad llega incluso a afectar a las definiciones globales de nuestro programa, que por medio de una tabla llamada _G son completamente accesibles.

Además de proporcionar una solución interna al ámbito de las variables, también proporciona un método al programador para poder redefinir absolutamente todo lo que afecta a la ejecución de su programa (por ejemplo, redefinir funciones de la librería estándar para que funcionen de forma distinta dependiendo del módulo en el que se ejecutan). Puede parecer un método arriesgado de proporcionar acceso a la información completa de un programa, pero además Lua proporciona mecanismos de seguridad (y muy sencillos, de nuevo haciendo un uso inteligente de las funcionalidades básicas del lenguaje) de forma que es el propio programador el que puede asegurarse de que sea su programa el que modifica ciertos datos y nadie más (en caso de que necesite hacer ese tipo de operaciones, que no serán las más habituales).

Es interesante que Lua deja la responsabilidad de programar bien al programador, lo que permite crear programas mucho más eficientes y hacerlo con unos requisitos mínimos en el lenguaje.

Programación Orientada a Objetos

De forma similar a como hemos visto que funcionan los módulos, podemos desarrollar una aproximación muy potente de lo que sería la Programación Orientada a Objetos (OOP) dentro de Lua, a pesar de que no es un lenguaje que lleve este paradigma escrito en sus genes. Para ello, de nuevo, bastará hacer uso de tablas, funciones, y añadir un mínimo necesario para que el pegamento sea cómodo de usar.

La forma de implementarlo en Lua es considerar, en primer lugar, que tanto objetos como clases son tablas. Comencemos por los objetos... y veremos una primera aproximación a la OOP, en la que con probabilidad faltarán detalles, pero que mostrará de nuevo la elegancia de las soluciones con un lenguaje mínimo pero bien pensado.

Un objeto dentro del paradigma OOP consta de un estado (conjunto de valores), una identidad que permite diferenciarlo del resto (normalmente, denotado como self o this, dependiendo del lenguaje), y un conjunto de operaciones que pueden trabajar con el objeto (y que normalmente se denominan métodos). ¿Qué problema hay para representar un objeto entonces por medio de una tabla, tal y como hacíamos con los módulos? (¿acaso un módulo no era un objeto sin identidad?)

Vamos a empezar a definir esta idea con un caso básico:

Hucha = { ahorros = 0 }
    
Hucha.sacar = function (c)
    Hucha.ahorros = Hucha.ahorros - c
end
    
Hucha.meter = function (c)
    Hucha.ahorros = Hucha.ahorros + c
end
    
Hucha.meter(100)
Hucha.sacar(10)
print (Hucha.ahorros)    

Gracias a que las funciones son datos de primera clase en Lua, algo tan sencillo como una tabla nos permite trabajar con ellas como si fueran objetos... pero no lo son. Por ejemplo, supongamos que queremos reaprovechar nuestro objeto Hucha para crear una hucha nueva. Nuestra primera intención sería escribir algo como h2 = Hucha, pero pronto nos damos cuenta de que h2 no es una hucha distinta, sino un nombre adicional para Hucha, algo que podemos comprobar fácilmente con:

h2 = Hucha
h2.meter(10)
print(h2.ahorros)
print(Hucha.ahorros)    

donde vemos que al modificar el contenido de h2 también se ha modificado el de Hucha.

El problema es que no hemos creado un sistema de clases para objetos, sino que hemos creado un objeto particular, y después lo hemos referenciado con una variable adicional (pero que referencia al mismo objeto).

Podemos resolver este problema añadiendo una identidad al objeto y haciendo que los métodos sepan de qué objeto se trata, por ejemplo:

Hucha = { ahorros = 0 }
    
Hucha.sacar = function (self,c)
    self.ahorros = self.ahorros - c
end
    
Hucha.meter = function (self,c)
    self.ahorros = self.ahorros + c
end
    
Hucha.meter(Hucha,100)
Hucha.sacar(Hucha,10)
print (Hucha.ahorros)    

Realmente, lo que estamos haciendo es obligar a que los métodos sepan sobre qué objeto concreto deben operar. Ahora, crear nuevas huchas se podría hacer de esta forma:

h1 = { ahorros = 0, meter = Hucha.meter, sacar = Hucha.sacar}
h2 = { ahorros = 0, meter = Hucha.meter, sacar = Hucha.sacar}
h1.meter(h1,100)
h2.meter(h2, 50)
print(h1.ahorros)
print(h2.ahorros)    

donde ya podemos ver que las huchas h1 y h2 son independientes... y dejamos Hucha como algo parecido a un prototipo para la creación de nuevos objetos de este tipo (más adelante lo haremos mejor).

A pesar de ello, y aunque hayamos conseguido simular el trabajo con objetos, e incluso podamos tener algo parecido a clases, parece poco elegante tener que pasarle el propio objeto al método para saber operar sobre él (a pesar de que, internamente, es lo que hacen todos los lenguajes OOP por medio de referencias... y realmente una variable no es más que una referencia al objeto al que apunta, como vimos en el primer ejemplo). Además, si modificamos un prototipo, hemos de ir repasando todos los objetos que se han creado a partir de él para estar seguros de que reflejarán también los cambios realizados.

Para calmar nuestra ansia estética, Lua proporciona un nuevo azúcar sintáctico que de alguna forma cubre el primero de esos problemas, y permite usar : como un medio para pasar la referencia al propio objeto como primer parámetro (es decir: a:f(x)=a.f(self,x)). Por lo que el código anterior se podría haber escrito como:

Hucha = { ahorros = 0,
    sacar = function (self,c)
        self.ahorros = self.ahorros - c
    end, 
    }
    
function Hucha:meter(c)
    self.ahorros = self.ahorros + c
end
    
h1 = { ahorros = 0, meter = Hucha.meter, sacar = Hucha.sacar}
h2 = { ahorros = 0, meter = Hucha.meter, sacar = Hucha.sacar}
h1:meter(150)
h2:meter(50)
print(h1.ahorros)
print(h2.ahorros)    

Observa que hemos escrito los dos métodos de forma distinta para reflejar las diferencias. Como el uso de : es solo notacional, si queremos usarla debemos definir el método de forma explícita, sin usar funciones anónimas (líneas 7-9). Y si queremos hacer uso de funciones anónimas para una definición compacta, entonces hemos de hacer uso de self (líneas 2-4). Posteriormente, en el uso de los métodos en objetos concretos, podemos ignorar el uso de self por medio del uso de : (líneas 13-14).

Obviamente, esta no termina siendo la forma más elegante y completa de usar OOP en Lua, ya que todavía carecemos de características habituales en objetos como: constructores, sobrecarga de operadores, herencia, privacidad, etc...

Para un uso más completo, cómodo, y apropiado de los objetos, Lua proporciona un uso adicional de las tablas en lo que se llaman las metatablas, que no son más que tablas que sirven para almacenar métodos genéricos para otras tablas (así, las metatablas serán como clases contenedoras, y contendrán los métodos adecuados para sus objetos). Basta pues añadir una funcionalidad a Lua para disponer de un sistema de clases más evolucionado: (cuidado, que son dos _ seguidos)

setmetatable(A, { __index = B})

Con esta instrucción, Lua consigue que B se convierta en un prototipo para A, de forma que cualquier clave que no tenga A, en vez de devolver nil, primero mirará en B para saber qué definición tiene y usarla (si no estuviera en B entonces sí devolverá nil). Es importante notar que no es un proceso complejo ni costoso, sino una simple referencia de una tabla a otra que permite almacenar en B aquello que define el comportamiento de A.

Vamos a usar metatablas para crear un constructor que facilite la creación de objetos asignados a la clase anterior:

Hucha = { ahorros = 0,
    sacar = function (self,c)
        self.ahorros = self.ahorros - c
    end, 
    }
    
function Hucha:meter(c)
    self.ahorros = self.ahorros + c
end
    
function Hucha.new(c)
    c = c or {}
    setmetatable(c,{__index=Hucha})
    return c
end
    
h1=Hucha.new{ahorros=10}
h2=Hucha.new{}
h1:meter(10)
print(h1.ahorros)
print(h2.ahorros)

Y vamos a analizar cómo funciona el método de creación (que hemos llamado new), lo que nos permitirá hablar de la útil forma en que Lua trabaja con operaciones booleanas.

Durante el proceso de creación se mira si el constructor ha recibido o no algún dato de entrada. Si no recibe ningún dato, c = nil, por lo que c or {} = {} (los operadores booleanos cortocircuitan en Lua, eso quiere decir que van evaluando secuencialmente los distintos operandos mientras sea necesario, y además devuelven el operando que lo hace parar; en el caso de or devolverá el primer operando que lo hace cierto, o false si ninguno lo hace cierto). Esto quiere decir que en el caso de h1, será c = {ahorros = 10}, y en el caso de h2, será c = {}. Posteriormente, se asigna {__index = Hucha} como metatabla, lo que significa que todo aquello que los nuevos objetos no encuentren en su propia tabla, lo buscarán en Hucha. Por ello, respecto a ahorros, h1 lo encuentra en su tabla (con el valor 10), pero h2 no, por lo que cuando le haga falta lo tomará de Hucha (con el valor 0). Como ninguno de estos objetos tienen definidos métodos, cuando se usen los tomarán de Hucha, trabajando pues como objetos de este tipo. Con esta nueva forma no es necesario repasar todos los objetos que usen como prototipo a Hucha, basta hacer una modificación en la definición de este prototipo para que automáticamente todos los objetos consideren la nueva definición, ya que la relación es dinámica.

Usando la notación anterior, podemos reescribir el constructor de otra forma, más común y clara:

function Hucha:new(c)
    c = c or {}
    self.__index = self
    setmetatable(c,self)
    return c
end

Aunque pueda parecer extraña esta forma de crear la relación entre el objeto y la clase, si se analiza un poco qué tablas intervienen en este proceso, llegaremos a la conclusión de que el proceso es el mismo que en el código anterior.

El sistema no puede ser más simple y económico, basta añadir una referencia a una tabla adicional, para poder trabajar con una clase completa con apenas una línea. La elegancia del lenguaje, de las decisiones que han ido tomando sus creadores, se muestra aquí con toda claridad.

Con ideas similares se puede implementar el concepto de herencia (y herencia múltiple), y privacidad. Por ejemplo, tengamos en cuenta que si un objeto tiene definido una clave, entonces ya no saltará a la definición de su clase, por lo que podemos considerar herencias parciales.

Corrutinas

Un corrutina, la tercera piedra fundamental de Lua, es una extensión del concepto de subrutina, pero con la diferencia principal de que se puede suspender su ejecución y retomarla cuando sea necesario. A diferencia de los otros conceptos de tablas y funciones, el problema con las corrutinas es que sus características no están tan estandarizadas y podemos encontrar implementaciones muy distintas (no equivalentes) en aquellos lenguajes de programación que sí las implementan.

En Lua las corrutinas son parecidas a los multi-hilos colaborativos, y verifican:

  • Son elementos de primera clase en el lenguaje también, de forma que se pueden almacenar en variables, pasarlas como parámetros a funciones, y ser devueltas por éstas (son el tipo de dato thread).
  • Pueden suspender su ejecución, disponiendo de su propia pila de llamadas, que se mantiene durante la suspensión para poder ser retomadas.
  • Son asimétricas, lo que quiere decir que se ofrecen dos controles para la transferencia de operaciones, resume y yield, que funcionan como un par llamada-devolución.

A pesar de todo, las corrutinas no tienen un uso tan extendido en Lua como las tablas y funciones (que son absolutamente ubicuas en el lenguaje), pero en cierto tipo de aplicaciones pueden jugar un papel fundamental por su capacidad para aumentar el control de ejecución de un programa.

Un uso habitual de las corrutinas se da en la implementación de multihilos cooperativos, aumentando la interactividad, por ejemplo, en sistemas reactivos, como pueden ser los juegos, donde diferentes elementos del juego pueden ejecutar su script en corrutinas separadas (un script será como un bucle sin fin en el que se actualiza el estado de dicho elemento y pasa su ejecución a otro elemento posteriormente), y todo el proceso puede ser controlado por un planificador sencillo. Otro entorno habitual en el que su uso es habitual es en los procesos en los que hay una jerarquía del tipo cliente-servidor, o cuando Lua interactúa con otros lenguajes de programación, donde las peticiones y ejecuciones se tienen que ir alternando.

Otro uso que personalmente veo muy interesante para las corrutinas es en la creación de iteradores para recorrer de forma personalizada estructuras de datos (que, no nos engañemos, son al fin y al cabo tablas con todos los sabores que uno quiera).

« NetLogo Wishlist « || Inicio ||