VISITAS:

viernes, 24 de enero de 2014

LUA: Lenguaje de programación

Qué es LUA

LUA es un lenguaje de programación de extensión. Lo cual significa que no es un lenguaje para escribir programas ejecutables stand-alone. En lugar de esto, LUA está pensado para integrarse dentro de un programa host. Este programa host es el que se encarga de lanzar y ejecutar el programa LUA. Además, el programa host puede declarar funciones escritas en C que pueden ser invocadas desde el programa LUA.
LUA ofrece soporte para los siguientes conceptos de programación:

  • programación orientada a objetos
  • programación funcional
  • programación data-driven

El concepto de lenguaje de extensión es similar a VBA en las aplicaciones de microsoft office.
LUA tiene un garbage collector, con lo cual no es necesario liberar los objetos que se van creando. Pero hay que tener cuidado con referencias a objetos que se quedan para siempre en alguna variable.

Léxico

LUA tiene un léxico basado en C:
  • Los punto y coma al final de sentencia son opcionales.
  • Los identificadores constan de letras, números y underscore (no pueden empezar con números; tampoco se recomienda que empiecen con underscore, ya que esta notación se utiliza para identificadores internos de LUA)
Bloques: son una secuencia de sentencias que termina en end (funciones, bucles, if, bloques do -- end)
Palabras reservadas: and, break, do, else, elseif, end, false, for, function, goto, if, in, local, nil, not, or, repeat, return, then, true, until, while.
Operadores que son distintos a otros lenguajes:
  • ~= distinto
  • .. concatenación de strings
  • ... número variable de argumentos en una función
Comentarios: 
  • -- comentario de una línea
  • --[[ comentario multilínea ]]--

Variables, Valores y Tipos

LUA es un lenguaje tipado dinámicamente, lo que significa que las variables pueden apuntar a valores de cualquier tipo y, además, pueden apuntar a valores de diferentes tipos en diferentes momentos. Por ejemplo:
v = 12;
v = "abc";
Tipos de datos:

  • nil : indica que a una variable no se le ha asignado todavía ningún valor
  • boolean : puede ser true o false
  • number : todos los números son reales, punto flotante doble precisión
  • string : es una cadena de caracteres
  • function : un bloque de código con nombre
  • table : es similar a los objetos en otros lenguajes de programación
La función global type() devuelve el tipo de una variable: nil, boolean, number, string, function, table.
Una variable que se declara sin ámbito, es accesible desde cualquier lugar del programa (posterior a su declaración).
Si se desea una variable con ámbito de bloque (lo cual es muy recomendable), hay que declararla con ámbito local:
local v = 12038;
Asignación a múltiples variables:
local x, y = 1, 2; -- x = 1; y = 2;
 Declaración de una tabla:
local t = { "a", "b", "c" };
Acceso a los elementos de una tabla:
local a = t[1];  -- El primer índice de una tabla es 1 (no 0)
Operador longitud #:
local s = "test";
local t = { 1, 2, 3 };
print(#s);    -- 4
print(#t);     -- 3

Tablas

Simplificando, las tablas son objetos también, como los strings, números, etc.
Realmente, son arrays asociativos, esto es, arrays de elementos que se acceden utilizando un identificador. El identificador, también llamado clave, puede ser un índice numérico o un valor alfanumérico. Las tablas son similares a los diccionarios o tablas hash de otros lenguajes, y comparten características con los arrays. Las tablas son la estructura de datos más potente de LUA. De hecho, son la única estructura de datos que tiene LUA. Con una tabla podemos representar arrays, conjuntos, registros, listas, mapas, etc. También se pueden utilizar tablas para representar módulos y objetos (ej. io.read() es el método read() del módulo io, sin embargo para LUA, es la clave read de la tabla io).
La forma más simple de declarar una tabla vacía es:
local table = { };
Un array es una tabla:
local a = { "a", "b", "c" };
local v = a[2];   -- v = "b"; 
Una tabla un poco más complicada:
local t = { one = "a", two = "b", three = "c" };
En ambos casos se utilizan las llaves para definir los elementos de la tabla, pero en el segundo caso, se da una clave específica a cada uno de los elementos. Esta clave se puede utilizar para acceder a cada uno de los elementos de una de las siguientes maneras:
t["two"];
t.two;
Se pueden añadir elementos a una tabla después de creada:
local t = { };
t.property1 = "abc";
t["property2"] = "fer";
t[3] = "hola";
Ejemplo de creación de una tabla y cómo rellenar sus elementos:
local t = { };
for i = 1, 100 do t[i] = tostring(i) end;
Se está creando una tabla de 100 elementos donde cada elemento contiene un string.
Si se accede a un elemento que no existe, obtendremos nil:
print(t[200]);  -- nil
print(t["x"]);  -- nil 
print(t.x);  -- nil
Es importante distinguir entre t.x, t["x"] y t[x]. Los dos primeros casos son equivalentes. El último depende del valor de la variable x.
Para eliminar un índice de una tabla simplemente hay que hacerlo nil:
local t = { a = 1, b = 2 };
t.a = nil;

Se puede utilizar el operador de longitud # para obtener el número de elementos que tiene una tabla.
Una tabla puede contener elementos de cualquier tipo (números, strings, funciones, o incluso otras tablas):
local tabla = {
    t = { prop1 = "hello" }
};
tabla.t.prop1;
Una tabla puede contener una función:
function f1()
end
local tabla = {
     fun1 = f1,
     fun2 = function() 
                end
}

Tablas como arrays

El equivalente LUA de un array es una tabla. Características de los arrays:

  • Todos los arrays en LUA tienen el índice base en 1 (no en 0 como en muchos lenguajes de programación). 
  • Los arrays necesitan tener un índice numérico. 
  • Los arrays pueden ser multidimensionales, haciendo que sus elementos sean a su vez arrays.
  • Los arrays no necesitan ser dimensionados en su declaración, ya que pueden crecer y dimensionarse en runtime.

Tablas como arrays asociativos (mapas)

El equivalente LUA para un array asociativo es una tabla. La diferencia entre un array asociativo y un array es que, en lugar de tener índices numéricos para acceder a los elementos del array, un array asociativo puede usar strings para acceder a sus elementos.
Se pueden mezclar en una misma tabla los dos tipos: array y array asociativo.

Tablas como objetos

Las tablas pueden usarse como objetos. Para ello podemos incluir funciones y datos en sus elementos.
Más adelante, en este mismo artículo, se verá la orientación a objetos en LUA.

Funciones

Una función en LUA se define con la palabra reservada function:
function add(n1, n2)
      return n1 + n2;
end
Nótese que los argumentos no tienen un tipo y por tanto se podrá pasar un valor de cualquier tipo a la función. Nótese también que no se especifica un tipo de valor retornado, y por tanto la función puede retornar cualquier valor (si no hay return dentro de la función, se retornará nil).
Una función puede retornar múltiples resultados:
function operaciones(n1, n2)
    return n1+n2, n1 - n2, n1 * n2;
end

Y se pueden recoger sus resultados en varias variables:
local x, y, z = operaciones(4, 6)  -- x = 10, y = -2, z = 24
 Una función puede aceptar un número variable de argumentos:
function fvar( ... )
    print(arg[1], arg[2], arg[3] );
end
 fvar(1, "a");  -- 1 a nill
Algunos argumentos pueden ser fijos, y otros variables:
function fvar2( a1, a2, ... )
    print(a1, a2, arg[1], arg[2], arg[3] );
end
Como ya sabemos, se pueden añadir funciones a una tabla. Pero la función no tiene el concepto de self (this de otros lenguajes) y por tanto no podrá acceder a otros datos de la tabla. Para que la función sea realmente un método de la tabla la notación tiene que ser:
local table = { a = 3, b = 3 };
function table:add(n1, n2)
    return self.a + self.b + n1 + n2;
end
Y para invocarla:

 table:add(4, 7);  -- 3 + 3 + 4 + 7 = 17

Funciones globales predefinidas

Realmente, en LUA una función global es una variable que tiene asignada una función.

 ipairs()

Permite recorrer los elementos de una tabla indexada numéricamente (no vale para tablas con identificadores).
for index, value in ipairs(table) do
end

pairs()

Permite recorrer los elementos de una tabla de cualquier tipo.
for index, value in ipairs(table) do
end

print()

 Imprime en consola todos sus argumentos separándolos por TAB.

tostring()

Convierte su argumento a string.
Lo interesante de este método es que cuando se aplica a una tabla que tiene definida una función en su propiedad __tostring, entonces llama a esa función.
Ejemplo:
local table = { a = "hola" };
function table:tostring() 
    return "a=" .. self.a; 
end
 setmetatable(table, { __tostring = table.tostring } );
print( table );   -- a=hola

type()

Retorna, como un string, el tipo de su argumento: Boolean, string, table, etc.

tonumber()

Convierte a número el parámetro de tipo string. Si el parámetro no representa un número, entonces retorna nil.

pcall()

Cuando ocurre un error en LUA, se propaga hacia arriba, normalmente hasta el programa host que lo maneja. En LUA no existe try/catch, pero existe pcall() que hace una función similar.
La llamada a pcall() incluye como parámetro la función que se quiere invocar y sus argumentos. Si ocurre un error dentro la función invocada, entonces pcall() retorna false y el error ocurrido. Si todo va bien, retorna true.
Ejemplo:
function badFunction(a, b)
    return a + b;
end
local result, error = pcall(badFunction,"1","2");

Estructuras de control

if

if condition1 then
    -- condition1 = true
elseif condition2 then
    -- condition2 = true
else
    -- all other cases

for

for variable = start, end, step do
    -- loop
end

while

while condition do
    -- loop
end

repeat

repeat
    -- loop
until condition;

Módulos

Cuando se escribe un programa LUA, se puede meter todo el código del programa en un único fichero main.lua, no hay problema sintácticamente hablando. Sin embargo, arquitecturalmente no es muy conveniente.
LUA ofrece el concepto de módulos para organizar mejor el código.
Desde el punto de vista de usuario, un módulo es un trozo de código LUA que puede ser cargado a través de require y que crea y retorna una tabla. Todo lo que el módulo exporta (funciones, constantes etc) se define dentro de esta tabla, la cual funciona como un namespace.
Por ejemplo, la librería estándar math es un módulo y se puede usar de la siguiente forma:
local m = require("math");
m.sin(2.4);
Sin embargo, el intérprete LUA precarga todas las librerías estándar con un código parecido al siguiente:
math = require("math");
string = require("string"); 
...
Con lo cual, podremos utilizar las librerías estándar como es habitual: math.sin(2.4);
Cada módulo sólo se carga una vez. Si más adelante, en otra parte del programa, se llama a require para volver a cargar un módulo ya cargado, LUA no lo carga, simplemente retorna la tabla que ya se ha cargado anteriormente. Si el módulo no se ha cargado anteriormente, require busca un fichero .lua con el nombre del módulo y lo carga.
Si un módulo lua se encuentra en un subdirectorio con respecto a main.lua, entonces la forma de cargarlo es la siguiente:


m = require("subdir.modulo");  -- carga modulo.lua que está en el directorio subdir/modulo.lua
La tabla package.loaded contiene todos los módulos cargados:
package.loaded.

La forma más sencilla de crear un módulo es la siguiente:

  1. creamos un fichero .lua con el nombre del módulo
  2. creamos una tabla local vacía
  3. ponemos en la tabla todas las funciones que queremos exportar
  4. retornamos la tabla

Ejemplo utils.lua:
local t_utils = { }; 
function utils.add(n1, n2) return n1 + n2; end; 
return t_utils;
Un ejemplo para manejar números complejos en LUA, fichero complex.lua:

local c = { }; 
-- Creación de un número complejo
function c.new (pr, pi) return { r = pr, i = pi }; end;
 
-- La constante i
c.i = c.new(0, 1);
 
-- Operaciones con números complejos
function c.add(c1, c2) return c.new(c1.r + c2.r, c1.i + c2.i); end;
function c.sub(c1, c2) return c.new(c1.r - c2.r, c1.i - c2.i); end; 
function c.mul(c1, c2) return c.new(c1.r * c2.r - c1.i * c2.i, c1.r * c2.i + c1.i * c2.r); end; 
local function inv(c) local n = c.r^2 + c.i^2; return c.new(c.r / n, -c.i / n); end; 
function c.div(c1, c2) return c.mul(c1, inv(c2)); end; 
function c.tostring(c) return "(" .. c.r .. "," .. c.i .. ")"; end; 
-- retornar la tabla
return c;
Utilización de este módulo:

local complex = requires("complex"); 
local c1 = complex.new(3,4); 
local c2 = complex.new(-3,6); 
local c3 = complex.div(c1, c2); 
print(complex.tostring(c3));

Metatablas

LUA tiene un conjunto de operaciones que son predecibles: podemos sumar dos números, concatenar dos strings, insertar una clave-valor en una tabla, etc. Sin embargo, no podemos sumar dos tablas o comparar dos funciones, por ejemplo. A menos que usemos metatablas.
Las metatablas nos permiten cambiar el comportamiento de un tipo de datos cuando se enfrenta a una operación no definida para ese tipo (recordemos que cada tabla es un tipo de datos distinto). Cuando intentamos sumar dos tablas, t1 + t2, LUA mira si alguna de ellas tiene metatabla y si la metatabla contiene la el miembro __add (que tiene que ser una función, llamado metamétodo en este caso). Si existe __add, LUA lo invoca para calcular la suma.
Cada tabla tiene su propia metatabla, pero los valores de otros tipos, números, strings, etc tienen una única metatabla para todos los valores de ese tipo.
Cuando se crea una nueva tabla, no tiene metatabla asociada. Podemos asociar una metatabla a una tabla mediante la siguiente función:
setmetatable(table, metatable);
En LUA sólo  podemos cambiar las metatablas de las tablas, no las metatablas de los demás tipos.

El metamétodo __tostring

Un uso típico de las metatablas es redefinir la función tostring() de una tabla. La función print(tabla) invoca a tostring(tabla), lo cual por defecto imprime algo que no es muy útil: 0x4724350. La función tostring() chequea si la tabla tiene una metatabla con la función __tostring. Así, para redefinir el comportamiento de tostring() para una tabla:
tabla =  { };
metatabla = { };
metatabla.__tostring = function(t)
    return ".....";
end
setmetatable(tabla, metatabla);

 El metamétodo __index

Como sabemos, si accedemos a un índice (o clave) de una tabla que no existe, el resultado es nil. Este es el comportamiento por defecto. Sin embargo, si la metatabla tiene un método denominado __index, en caso de no encontrar el índice, se invoca este metamétodo y se retorna su resultado.

Programación orientada a objetos en LUA

En LUA, una tabla es un objeto. Igual que los objetos, las tablas tienen estado. Como los objetos, las tablas tienen identidad, self.Y también, como los objetos, las tablas tienen un ciclo de vida.
Veamos otros aspectos de las tablas/objetos:

Operaciones

La siguiente definición crea una nueva función y la almacena en el campo retirar del objeto Account:
Account = { balance = 0 };
function Account.retirar(v) Account.balance = Account.balance -  v; end;
A esto casi se le podría llamar método de un objeto, pero no es exactamente, ya que utiliza dentro de la función a la variable global Account. Por tanto, sólo funcionará para ese objeto concreto.
Una primera solución es pasar como parámetro a la función el objeto sobre el que aplica:
function Account.retirar(self, v) self.balance = self.balance - v; end;
En los lenguajes orientados a objetos, self (o this) se suele ocultar en las llamadas y se pasa implícitamente. Esto también se puede hacer en LUA utilizando : en lugar de .
Account =  { balance = 0 }; 
function Account:retirar(v) self.balance = self.balance - v; end;
function Account:ingresar(v) self.balance = self.balance + v; end; 
a = Account;
a:ingresar(200); 
a:retirar(100);
El siguiente problema es cómo crear objetos del mismo tipo, o sea, cómo definir clases.
¿Cuál es la diferencia entre invocar a una función con . y con : ? Cuando usamos . le estamos diciendo a LUA que la función es un miembro de la tabla, del objeto tabla. Cuando usamos : le estamos diciendo a LUA que pase un parámetro oculto a la función que es el propio objeto, o sea, self.
Veamos un ejemplo sencillo:
a = { };
a.move1 = function (self, v)
    print("self = ", self, " v = ", v);
end
function a:move2(v)
    print("self = ", self, " v = ", v);
end
Ejemplos de uso:
a.move1(10);  -- self = 10 v = nil
a.move1(a,10);  -- self = 0x3457563 v = 10
a:move1(10);   -- self = 0x3457563 v = 10
a:move2(10);   -- self = 0x3457563 v = 10
a.move2(10);    -- self = 10 v = nil

Clases

Una clase es como un molde para crear objetos. Todos los lenguajes orientados a objetos ofrecen el concepto de clase. Pero LUA no. Sin embargo, es fácil emular este comportamiento. La idea es crear un objeto que sirva como prototipo para crear otros objetos:
Account = { balance = 0 }; 
function Account:new()
    obj = { };
    setmetatable(obj, self);
    self.__index = self;
    return obj;
end;
function Account:retirar(v)
    self.balance = self.balance - v;
end;
obj1 = Account:new();
obj2 = Account:new();
obj1:retirar(10);

















9 comentarios:

  1. Gracias por toda esta información, es de mucho interés. :)

    ResponderEliminar
  2. Gracias a ti Marc. Espero que te haya servido.

    ResponderEliminar
  3. tienes algun ejemplo de template??? quiero poner fondos y una que otra animación básica

    ResponderEliminar
  4. HOLA AMIGO PUEDES HACER UN VIDEO EN EL QUE ENSEÑES A CREAR UN JUEGO CON LUA

    ResponderEliminar
  5. Hola! Buenas... Gracias a su blogger, realicé mi tarea...Felicidades! Bendiciones :)

    ResponderEliminar
  6. Se Puede Entender Aún Más En Su Blogger...

    ResponderEliminar
  7. Se Puede Entender Aún Más En Su Blogger...

    ResponderEliminar
  8. Hola! Buenas... Gracias a su blogger, realicé mi tarea...Felicidades! Bendiciones :)

    ResponderEliminar
  9. Tu post es genial. Muy bien explicado. Gracias!!, es de una gran utilidad por su sencillez.

    ResponderEliminar