Introducción a la programación en Lua

En este tutorial se presentan los conceptos básicos del lenguaje de programación Lua, sirviendo de introducción para aprender este lenguaje de programación.

Introducción

Lua es un lenguaje de programación ligero, desarrollado sobre el lenguaje C, de código abierto. Ha sido diseñado para ser embebido en otras aplicaciones convirténdose en un poderoso lenguaje de script para expandir y personalizar características en una aplicación.

Lua fue desarrollado en la Universidad Católica Pontificia de Rio de Janeiro en 1993 pour un equipo de investigadores formado por Roberto Ierusalimschy, Waldemar Celes y Luiz Henrique Figueredo.


Características de Lua

Entre las características más destacables de Lua podemos citar:


Usos de Lua

Los principales usos de Lua son:

Instalación de Lua

En este artículo analizaremos la instalación de Lua en las principales plataformas: Windows, Linux y MacOS X.

Instalación en Windows

La instalación en Windows es muy sencilla. Descarga la última versión de Lua desde el repositorio en SourceForge, actualmente la version 5.4.2:

Adicionalmente, en la siguiente dirección encontrarás los binarios para las últimas versiones de Lua. Dentro de cada versión, en la carpeta "Tools Executables" encontraras los ejecutables ya compilados para cada plataforma.

Una vez descargado el archivo, descomprímelo en C:\Lua. Deberías tener cuatro archivos:

Para finalizar la instalación y simplificar el uso de Lua, puedes registrar la ruta C:\Lua en la variable "PATH" de Windows.


Instalación en Linux

La instalación en Linux es tan simple como en Windows. Primero vamos a descargar los archivos binarios en el ordenador y posteriormente los descomprimimos. Para finalizar instalamos Lua usando el script de instalación. Para instalar Lua puedes usar los siguientes comandos:

curl -R -O http://www.lua.org/ftp/lua-5.4.2.tar.gz tar zxf lua-5.4.2.tar.gz cd lua-5.4.2
make linux test
make install

Instalación en MacOS X

La instalación en MacOS X es casi idéntica a la instalación en Linux. Para instalar Lua en MacOS X puedes usar los siguientes comandos:

curl -R -O http://www.lua.org/ftp/lua-5.4.2.tar.gz tar zxf lua-5.4.2.tar.gz cd lua-5.4.2
make macosx test
make install

Prueba de la instalación

Una vez finalizada la instalación solo nos queda que probar Lua. Crea un nuevo archivo al que llamaremos HolaMundo.lua y escribe el siguiente código:

print("¡Hola Mundo!")

Prueba en Windows

En sistemas Windows puedes cambiar el nombre del ejecutable para simplificar el uso. Renombra lua54.exe a lua.exe.

Recuerda que si no has definido la ruta al ejecutable de Lua en la variable PATH, tendrás que usar las rutas completas al ejecutable y/o al archivo HolaMundo.lua.

C:\> lua HolaMundo.lua ¡Hola Mundo!

Prueba en Linux y MacOS X

Tanto en Linux y en MacOS X puedes usar el siguiente comando para probar tu instalación:

$ lua HolaMundo.lua ¡Hola Mundo!

Modos de uso del intérprete

Lua tiene dos modos de uso diferentes: el modo interactivo y el modo script. A continuación te explicamos con más detalle estos dos modos.

Para simplificar los ejemplos usaremos el símbolo de sistema de Unix ($), si usas Windows esto representa a por ejemplo C:>.

Modo interactivo

El modo interactivo permite la ejecución del código instrucción a instrucción. Todas las instrucciones son analizadas y ejecutadas inmediatamente y si hay algún resultado, se muestra inmediatamente. Para arrancar el modo interactivo, ejecutamos el comando lua sin argumentos. Veamos un ejemplo:

$ lua 
Lua 5.2.4 Copyright (C) 1994-2015 Lua.org, PUC-Rio 
>

El caracter > sirve para indicar que el intérprete de Lua está listo para aceptar instrucciones. Prueba el modo interactivo con el siguiente código:

> print("¡Hola Mundo!")

Después de cada instrucción presiona la tecla ´Entrar´ para que el intérprete de Lua evalúe la instrucción. Si la instrucción tiene un resultado este será mostrado a continuación. En el caso de nuestro ejemplo la instrucción ´print()´ imprime la cadena especificada en la consola:

> print("¡Hola Mundo!") 
¡Hola Mundo! 
>

Escribe ahora el siguiente código el intérprete interactivo y recuerda que después de cada instrucción debes pulsar la tecla Entrar:

> a = 2 
> b = 2 
> print(a+b) 
4 
>

En el ejemplo anterior, vemos que las dos primeras instrucciones son evaluadas pero no tienen ninguna salida, porque se trata de asignaciones. La tercera instrucción, como sabes, sí produce una salida, en este caso el valor de la suma de las variables a y b. Este es un ejemplo de instrucciones que son evaluadas y almacenadas en memoria para su uso posterior.


Modo script

En el modo script, a diferencia del modo interactivo en el que escribimos cada una de las instrucciones en la consola, le pasamos al intérprete un archivo en el que se definen todas las instrucciones a evaluar. Esto significa que el intérprete leerá y evaluará las instrucciones, linea a linea e irá mostrando salidas por la consola en función de las instrucciones, ejecutando la integralidad del archivo. El archivo en el que se definen todas las instrucciones recibe el nombre de script. Vamos a repetir el ejemplo anterior, pero esta vez escribiremos las instrucciones en un archivo. Crea un nuevo fichero con el nombre PruebaScript.lua y escribe las siguientes instrucciones:

a = 2 b = 2 
print("¡Hola Mundo!") 
print(a+b)

Ahora vamos a ejecutar el script usando Lua. Escribe el siguiente comando en la línea de comandos:

$ lua PruebaScript.lua 
¡Hola Mundo! 
4

Si observas verás que es la misma salida que en el ejemplo interactivo, ya que de hecho, son las mismas instrucciones pero introducidas de modo diferente. Puede que te estés preguntando para qué sirve uno y otro modo. Sin entrar mucho en detalles, el uso más común del modo interactivo sería para el prototipaje, esto es, para hacer pruebas de conceptos. Si estás desarrollando un algoritmo y quieres saber qué resultado produce una instrucción, si no es muy compleja, el modo interactivo te ofrece una forma sencilla y rápida de hacer esa prueba. Por otro lado, el modo script es más adecuado para ejecutar programas más complejos o bien programas que deseas persistir. Esto último es importante, porque el código que escribas en el modo interactivo se pierde al salir del modo interactivo.

Sintaxis básica

En este apartado vamos a repasar la sintaxis básica de Lua.

Comentarios de una sola línea

Escribe el caracter - (guion) dos veces al inicio de la línea:

-- Este es un comentario de una sola línea

Comentarios multilínea

Usa la combinación:

--[[
    Este es un comentario en
    múltiples líneas.
]]--

Identificadores

Los identificadores son los nombres usados para las variables, funciones, módulos, etc. Lua define una serie de requisitos para la definición de identificadores. Los caracteres permitidos son:

Los identificadores deben comenzar obligatoriamente por una letra mayúscula, minúscula o guion bajo (_), seguidas de cero o más caracteres alfanuméricos.

Ten en cuenta que Lua diferencia las mayúsculas de las minúsculas, así, el identificador miVariable es diferente del identificador MiVariable.

Algunos ejemplos de identificadores válidos son:

myVariable  contador1   Verificar_Valor _temporal
i1          j           indice          TEST

Palabras reservadas

Lua, al igual que todos los lenguajes de programación, define una serie de palabras que están reservadas para Lua. Todas estas palabras constituyen las instrucciones básicas del lenguaje de programación de Lua y no se pueden usar para definir identificadores. A continuación se muestra la lista de las palabras reservadas de Lua:

and
break
do
else
elseif
end
false
for
function
if
in
local
nil
not
or
repeat
return
then
true
until
while


 

Variables globales

En Lua las variables son consideradas generalmente como globales. Para que una variable sea considerada como global debe estar declarada en el ámbito global, es decir fuera de cualquier módulo o función:

$ lua
> a = 1
> function SetA ()
>> aa = 2
>> end
> print(a)
1
> print(aa)
nil

En el ejemplo anterior, se define una variable a, que es global a todas las instrucciones. Luego se define una función SetA en la que se define a su vez una variable aa. Cuando imprimimos la variable a, vemos que está definida ya que devuelve un valor. Sin embargo cuando imprimimos la variable aa nos devuelve nil que es el identificador de valor nulo. Esto significa que el ámbito donde se definió aa no es global, es decir, no están en el mismo ámbito.

Toda variable que haya sido definida, puede ser anulada asignándole el valor nil:

a = nil
print(a) --> Devuelve nil

Esto significa que esta variable dejará de existir en el ámbito donde se realizó la asignación. Si una variable tiene un valor diferente de nil, esta estará disponible en su ámbito.

Tipos de datos

Lua es un lenguaje de tipos dinámicos. Al igual que en otros lenguajes dinámicos, el tipo de una variable es inferido en función del valor asignado. En esta afirmación hay que tener en cuenta dos conceptos:

Cuando hablamos de lenguajes de tipos estáticos, como por ejemplo C, C++, C#, Java entre otros, realizamos las dos operaciones. Primero declaramos la variable, que tiene que ser obligatoriamente de un tipo y luego le asignamos un valor, que debe ser concordante con el tipo declarado.

En Lua y en otros lenguajes dinámicos, se hacen las dos operaciones durante la asignación. Esto quiere decir que cuando hacemos una asignación a = "Valor", estamos declarando una variable a, a la cual le asignamos el valor "Valor". A partir del valor asignado, el intérprete de Lua es capaz de 'adivinar' el tipo. En el caso de nuestro ejemplo, "Valor" es un literal de cadena y entonces la variable a será del tipo string. Dicho esto, si intentamos declarar una variable en Lua sin asignarle un valor, el intérprete nos dará un error.


Tipos básicos de Lua

En Lua se definen ocho tipos básicos:

nil Representa un valor nulo o inválido
boolean Representa un valor booleano: verdadero, falso
number Representa un número real en coma flotante de doble precisión
string Representa una cadena de caracteres
function Referencia a una función declarada
userdata Representa cualquier otro tipo de datos almacenados en la variable
thread Referencia un hilo para entornos multi-hilo
table Referencia a un array asociativo multidimensional

Podemos conocer el tipo de una variable en todo momento usando la función type():

print(type(11.6))                --> number
print(type("Hola Mundo"))        --> string
print(type(print))               --> function
print(type(false))               --> boolean
print(type(nil))                 --> string
print(type({a = 1}))             --> table

nil

El tipo nil indica que no hay valor definido. Si el valor es retornado para una variable, quiere decir que la variable no está definida. Si este valor es retornado por una función significa que no hay valor a devolver. Por ejemplo:

> print(a)
nil

En el ejemplo anterior se intenta imprimir el valor de una variable a, pero el valor devuelto es nil. En este caso significa que la variable no ha sido declarada y por lo tanto no tiene valor. Recuerda que toda variable que se le asigne el valor nil desaparece, porque para que exista, debe tener un valor asignado diferente de nil.

Cuando se asigna nil a un elemento de una tabla, al igual que en las variables, este elemento es anulado y por consiguiente, eliminado. Si se asigna nil a una variable que tiene una referencia a una tabla, la tabla entera será anulada y eliminada. Escribe el siguiente código en un archivo y ejecútalo con Lua para ver un ejemplo que te permita comprender mejor este concepto:

tabla1 = { elm1 = "val 1", elm2 = "val 2", elm3 = "val 3" }

for k, v in pairs(tabla1) do
    print(k .. " _ " .. v)
end

tabla1.elm2 = nil

for k, v in pairs(tabla1) do
    print(k .. " _ " .. v)
end

El resultado es el siguiente:

elm3 _ val 3
elm2 _ val 2
elm1 _ val 1
elm3 _ val 3
elm1 _ val 1

Podemos ver que las tres primeras lineas corresponden a los tres elementos de la tabla. Pero en las dos últimas, falta el elemento elm2 que hemos eliminado asignándole nil.


boolean

Los tipos boolean representan dos posibles valores: true o false. Puedes probar el siguiente código para entender mejor el concepto:

a = true
b = false

function PrintValue(value)
    if value then
        print("Verdadero")
    else
        print("Falso")
    end
end

PrintValue(a) --> a = true
PrintValue(b) --> b = false

El ejemplo produce la siguiente salida:

Verdadero
Falso

number

En Lua existe un solo tipo numérico con el que se pueden representar varios formatos numéricos, por ejemplo:


string

Este tipo representa una cadena de caracteres. Esta cadena se puede delimitar usando comillas dobles (") o comillas simples (') indistintamente. Así por ejemplo son expresiones de cadenas válidas:

cadena1 = "Cadena con comillas dobles"
cadena2 = 'Cadena con comillas simples'

Cuando queremos asignar una cadena que contiene múltiples lineas, podemos usar dobles corchetes "[[]]" para englobar toda la cadena:

text = [[ Lorem ipsum dolor sit amet, consectetur adipiscing elit. 
Vivamus ornare viverra augue, nec auctor sem pellentesque ac. 
Nulla at varius quam. 
Ut velit augue, ornare a sapien et, ultricies egestas mauris.
Nam augue nibh, vulputate ut sagittis eu, vulputate imperdiet dolor.  ]]

Si realizamos una operación matemática entre un tipo numérico y un tipo string, el intérprete de Lua intentará convertir la cadena de texto en un valor numérico y realizar la operación después. Si la cadena no puede ser convertida a un número el intérprete dará un error:

> print(8 + "2")
10
> print("8" + "2")
10
> print("8 + 2")
8 + 2
> print("3e2" * "3")
900
> print("numero" + 5)
stdin:1: attempt to perform arithmetic on a string value
stack traceback:
stdin:1: in main chunk
[C]: in ?

Dos o más cadenas de texto se pueden concatenar usando dos puntos consecutivos (..) entre las cadenas o variables:

> a = "Conca"
> b = "tenación"
> print(a .. b)
Concatenación
> print("Conca" .. "tenación")
Concatenación

Se puede obtener la longitud de una cadena usando el operador almohadilla (#):

> cadena = "Longitud"
> longitud = #cadena
> print(longitud)
8
> print(#cadena)
8
> print(#"Longitud")
8

table

El tipo table corresponde a un array asociativo, esto significa, que podemos usar números y cadenas para indizar los elementos. Del mismo modo, el array puede ser multinivel. Podemos inicializar una tabla vacía o añadiendo elementos:

tabla1 = {}

tabla2 = { "naranja", "manzana", "piña", "plátano" }

En una tabla, los índices no tienen que ser necesariamente del mismo tipo, pudiendo existir índices de tipo cadena y de tipo numérico:

tbl = {}
tbl["indice"] = "valor"

i = 5
tbl[i] = 10
tbl[i] = tbl[i] + 5

for k, v in pairs(tbl) do
    print(k .. " : " .. v)
end

Si ejecutamos el código anterior obtenemos el siguiente resultado:

$ lua TestTableIndices.lua
indice : valor
5 : 15

Las tablas en Lua son de índice 1, esto significa que por defecto, el primer elemento de una tabla cuando no se especifiquen índices explícitamente será 1. Esto marca una diferencia con multitud de lenguajes cuyos arrays son de índice 0. Veamos un ejemplo:

tabla = { "naranja", "manzana", "pera", "uva" }

for k, v in pairs(tabla) do
    print("Índice: " .. k .. ", Valor: " .. v)
end

Cuyo resultado es:

$ lua TestTableIndices2.lua
Indice: 1, Valor: naranja
Indice: 2, Valor: manzana
Indice: 3, Valor: pera
Indice: 4, Valor: uva

Las tablas en Lua son dinámicas por lo que no tienen un tamaño definido. Una tabla irá ajustando su tamaño en función del número de elementos que contenga automáticamente. Eso quiere decir, que es el intérprete de Lua quien se ocupa de gestionar la memoria necesaria para almacenar los elementos de la tabla.


function

El tipo function almacena una referencia a una función. Esto significa que podemos asignar una función a una variable para usarla después:

function factorial(n)
    if n == 0 then
        return 1
    else
        return n * factorial(n - 1)
    end
end

print(factorial(4))
factorialVar = factorial
print(factorialVar(4))

Al ejecutar el código anterior obtenemos el siguiente resultado:

$ lua TestTipoFunction.lua
24
24

El tipo function también acepta las funciones anónimas pasadas como parámetros:

function Anonima(tbl, fun)
    for k, v in pairs(tbl) do
        print(fun(k, v))
    end
end

tabla = { clave1 = "valor1", clave2 = "valor2" }

Anonima(tabla, function(clave, valor)
    return clave .. " = " .. valor\nend)

El resultado del código anterior es el siguiente:

$ lua TestFunctionAnonima.lua
clave2 = valor2
clave1 = valor1

thread

Las variables de tipo thread contienen referencias a hilos gestionados por el sistema operativo. Estos hilos permiten la realización de tareas en paralelo pero, pueden ser bastante difíciles de manejar. Como alternativa a los hilos, Lua maneja las llamadas corutinas, que permiten cierto paralelismo usando un solo hilo. Las corutinas son gestionadas directamente por el intérprete de Lua.


userdata

Las variables de tipo userdata contienen referencias a datos arbitrarios de C de cualquier tipo. En este tipo de datos se suelen almacenar referencias a estructuras y punteros.

Variables

Las variables son referencias a espacios de memoria donde se almacenan los datos de la aplicación. Las variables pueden ser de cualquier tipo soportado por Lua. En todo caso, antes de poder usar una variable esta debe ser declarada. Si intentamos acceder a una variable que no ha sido declarada se devuelve nil.

Existen tres tipos de variables: locales, globales y campos de tabla. Las variables globales son accesibles accesibles por todo el script. Las variables locales son accesibles solamente en el bloque donde fueron declaradas. Para declarar una variable como local debemos usar la palabra clave local delante del identificador de la variable:

local a = 10    --> Variable local

Las variables globales se declaran directamente sin usar ninguna palabra clave:

a = 10    --> Variable global

En el siguiente ejemplo vamos a definir una variable global y una local, para luego intentar acceder a éstas desde los dos ámbitos:

a = 10

function Local()
    local b = 5
    
    print("Local: a -> " .. a .. ", b -> " .. b)
end

Local()
print("Global: a -> " .. a .. ", b -> " .. b)

Cuyo resultado es:

$ lua TestAmbitosVariables.lua
Local: a -> 10, b -> 5
lua: TestAmbitosVariable.lua:10: attempt to concatenate global 'b' (a nil value)
stack traceback:
TestAmbitosVariable.lua:10: in main chunk
[C]: in ?

Como puedes observar, la llamada a la función Local() se ejecuta correctamente, la variable global a y la variable local b se pueden acceder desde la función sin problemas. Sin embargo, la última llamada a print() produce un error: b es nil. Dado que b fue definida como local dentro de la función, por lo que, esta está disponible solamente dentro de dicha función, fuera de ella, simplemente no existe.


Declaración y asignación

Toda variable en Lua debe ser declarada e inicializada antes de ser utilizada. A diferencia de lenguajes de tipos estáticos como C, C++, C#, Java, etc., Lua no permite la declaración de una variable sin inicializarla. Solo existe una excepción a esta regla y es en el caso de los parámetros de funciones u otras estructuras del lenguaje, como el bucle for. En estos casos sí se declara una variable sin inicializar, posteriormente, en tiempo de ejecución, el intérprete le asignará los valores en función del flujo del programa. El siguiente código es correcto en C#:

string miCadena;
bool condicion = true;

if (condicion)
    miCadena = "Verdadero";
else
    miCadena = "Falso";

Console.WriteLine(miCadena);

Cuyo resultado es:

Verdadero

Sin embargo, el código anterior no funcionaría en Lua. En la primera línea se declara una variable de tipo string a la cual no se le asigna ningún valor. Esta variable tendrá entonces un valor null. Más tarde en el bloque condicional se le asigna un valor a esa variable. En Lua no podemos declarar una variable sin asignarle un valor y esto es debido a los tipos dinámicos. Si en Lua intentamos declarar una variable sin asignarle un valor se produce un error.

Lua maneja dinámicamente el tipo de las variables. Como no podemos definir el tipo del mismo modo que se hace en C#, el único modo que tiene el intérprete de conocer el tipo es por la expresión que se le asigne. Esto se llama inferencia de tipos.

En el siguiente código de ejemplo, declaramos una función y una tabla donde asignamos varios elementos de diferentes tipos:

function Funcion(valor)
    print(type(valor))
end
    
a = {
    "Texto",
    15.9,
    Funcion,
    false,
    {}
    
}

for k, v in pairs(a) do
    Funcion(v)
end

Cuando ejecutamos este código obtenemos la siguiente salida:

$ lua TestTiposAsignados.lua
string
number
function
boolean
table

Como puedes observar en el ejemplo anterior, Lua ha reconocido los tipos de las variables a partir de las expresiones asignadas. En el apartado Tipos de datos tienes más información acerca de los diferentes tipos disponibles en Lua y sus expresiones.


Asignación de múltiples variables

Lua permite la asignación de varias variables al mismo tiempo:

a, b = "texto", 5

Los valores son asignados a las variables en el mismo orden, por lo que el ejemplo anterior equivale a:

a = "texto"
b = 5

En el caso de la asignación de variables cruzadas, Lua primero, evaluará las variables a la derecha para luego hacer la asignación. De este modo podemos invertir dos variables:

x, y = y, x                 --> Inversión del valor de las variables x e y
a[i], a[j] = a[j], a[i]     --> Inversión del valor de los elementos i y j

Si el número de elementos de la izquierda no es igual al de elementos en la derecha se siguen las siguientes reglas:

> x, y, z = 1, 2
> print(x, y, z)
1 2 nil
> x, y = x+1, y+2, z+3
> print(x, y)
2, 4
> x, y, z = 0
> print(x, y, z)
0 nil nil

También se pueden asignar directamente a varias variables el retorno de una función que devuelva varios valores:

x, y = obtenerCoordenadas()

En este ejemplo, se asume que la función obtenerCoordenadas() devuelve dos valores que serán asignados a las variables x e y.

Es posible el uso de variables locales en las asignaciones múltiples.


Índice

Los índices de las tables se pueden especificar de dos modos:

table[i]
table.i 
> table = { 10, 20, i3 = 30 }
> print(table[1])
10
> print(table.2)        
stdin:1: ')' expected near '.2'  -- Produce un error porque es un elemento de tipo lista
> print(table[2])
20
> printe(table.i3)      -- Es un elemento de tipo clave/valor
30

Operadores

Los operadores son unos signos gráficos que indican al intérprete que debe realizar operaciones lógicas, matemáticas o relacionales. Lua implementa varios tipos de operadores como veremos a continuación.

En todos los ejemplos a continuación se asume que el valor de la variable x es 5 y el de la variable y es 10.

Operadores aritméticos

Los operadores aritméticos, como su nombre indica, son aquellos usados para la realización de operaciones matemáticas. Estos operadores permiten todas la operaciones básicas y se describen a continuación:

Operador Descripción Ejemplo
+
Suma dos operandos x + y = 15
-
Resta el segundo operando al primero x - y = -5
*
Multiplica ambos operandos x - y = -5
/
Divide Numerador por denominador x / y = 0.5
%
Módulo, devuelve el resto de la división y % x = 0
^
Exponente, eleva un número a otro x ^ 3 = 125
-
Unario, cambia el signo del valor -x = -5

Operadores relacionales

Los operadores relacionales permiten realizar la comparación entre valores. Estos operadores son los usados en las estructuras de control de flujo y bucles para definir las condiciones.

Operador Descripción Ejemplo
==
Devuelve true si ambos operandos son iguales (x == y) -> false
~=
Devuelve true si ambos operandos son diferentes (x ~= y) -> true
>
Devuelve true si el operando de la izquierda es mayor que el de la derecha (x < y) -> true
<
Devuelve true si el operando de la izquierda es menor que el de la derecha (x < y) -> true
>= Devuelve true si el operando de la izquierda es mayor o igual que el de la derecha (x >= y) -> false
<= Devuelve true si el operando de la izquierda es menor o igual que el de la derecha (x <= y) -> true

Operadores lógicos

Los operadores lógicos permiten la realización de operaciones con expresiones de condición. Estos operadores se usan comúnmente en estructuras de control de flujo y bucles. En este ejemplo se asumen las variables a = false y b = true.

Operador Descripción Ejemplo
and Operador de conjunción copulativa. Devuelve true si ambas expresiones a izquierda y derecha son true. False en caso contrario a and b -> false
or Operador de conjunción correlativa. Devuelve true si una de las expresiones a izquierda o derecha son true. False en caso contrario a or b -> true
not Operador de negación. Invierte el estado lógico de la expresión a la que precede not (a == b) -> true

Otros operadores

Aparte de los operadores anteriores, Lua incluye dos operadores suplementarios, el operador de concatenación y el operador contador.

Operador Descripción Ejemplo
.. Concatenación de cadenas a = "Hola" .. " " .. "Mundo" -> Hola Mundo
# Devuelve la longitud o número de elementos de una cadena o una tabla #"Hola Mundo" -> 10

Precedencia de los operadores

Cuando se escriben expresiones complejas se tornan necesarias las reglas de precedencia, permitiendo así al intérprete evaluar dichas expresiones. Es importante conocer bien estas reglas porque determinan de qué forma se evaluarán las expresiones. Si escribimos expresiones sin tener en cuenta las reglas de precedencia podrían producirse evaluaciones con resultados inesperados.

Las reglas de precedencia definen entonces como se agruparán los elementos de una expresión. Básicamente estas reglas definen el orden de precedencia de los diferentes operadores. Aquellos operadores que tienen mayor precedencia serán evaluados primero. A continuación se muestra la tabla de precedencia de operadores que están ordenados de mayor a menor precedencia:

Orden Categoría Operador Evaluación
1
Unarios not # - De derecha a izquierda
2
Concatenación .. De derecha a izquierda
3
Multiplicativos * / % De izquierda a derecha
4
Aditivos + - De izquierda a derecha
5
Relacionales < > <= >= == ~= De izquierda a derecha
6
Igualdad == ~= De izquierda a derecha
7
Conjuntivos
and De izquierda a derecha
8
Disyuntivos
or De izquierda a derecha

Cadenas

Las cadenas son un tipo de expresión usadas para la definición de textos de longitud arbitraria. A través de las cadenas podremos definir todos los textos que sean necesarios en nuestra aplicación. El intérprete de Lua tiene un soporte limitado para la gestión de cadenas y se limita a la definición de cadenas y la concatenación de las mismas. A través de la librería string, tendremos acceso a funciones avanzadas de manipulación que veremos en este artículo.


Definición

Una cadena está formada por una sucesión de caracteres así como una serie de caracteres de control, como por ejemplo, retorno de carro. Una cadena en Lua se puede inicializar de tres formas distintas:

Este ejemplo a continuación muestra los tres tipos de definición:

cadena1 = "Tutorial de Lua 1"
cadena2 = 'Tutorial de Lua 2'
cadena3 = [[Tutorial de Lua 3]]

print("La cadena 1 es: ", cadena1)
print("La cadena 2 es: ", cadena2)
print("La cadena 3 es: ", cadena3)

El resultado que obtendremos es el siguiente:

La cadena 1 es:         Tutorial de Lua 1
La cadena 2 es:         Tutorial de Lua 2
La cadena 3 es:         Tutorial de Lua 3

Caracteres de escape

Los caracteres de escape son secuencias de caracteres usadas en una cadena de caracteres y que permiten modificar la interpretación de éstos. Por ejemplo, en una cadena delimitada por comillas dobles, queremos usar comillas dobles dentro de la cadena, para ello las comillas dobles las representaremos usando \": "Mi texto \"entre comillas\"". A continuación se muestra una lista de caracteres de escape.

Caracteres de escape
Uso
ASCII (decimal)
\n
New line
10
\r
Carriage return
13
\t
Tab
9
\v
Vertical tab
11
\\

Barra invertida

92

\"
Comillas dobles 34
\'
Comillas simples
39
\[
Corchete izquierdo
91
\]
Corchete derecho
93

Concatenación de cadenas con el operador ..

El operador .. permite la concatenación de cadenas en Lua, concatenando la expresión situada a la derecha del operador, a aquella que se encuentra a su izquierda.

> print("Tutoriales " .. "de " .. "Lua")
Tutoriales de Lua
> cadena = "Tutoriales " .. "de " .. "Lua"
> print(cadena)
Tutoriales de Lua

Manipulación de cadenas en Lua con la librería string

El intérprete de Lua provee un soporte muy limitado de la gestión de cadenas, proveyendo solamente las operaciones de definición y concatenación de cadenas. Para las operaciones más complejas, tales que, extracción de subcadenas, cálculo de longitud, conversiones, etc., Lua dispones de una una serie de funciones que permiten la manipulación de cadenas de una manera sencilla. Estas funciones están definidas en la librería string. A continuación veremos las diferentes funciones con más detalles así como algunos ejemplos de uso.

La funciones de la librería string usan el índice 1. Esto significa que las funciones que devuelven el índice de un caracter, para el primer caracter, devuelven 1 (en otrols lenguajes devuelven 0). 

Contenidos

string.find

string.find(cadena, patrón, [,inicio [,plano)

Busca la primera ocurrencia del patrón en la cadena. Si encuentra el patrón, devuelve un par de valores que contienen la posición de inicio y final. Si no encuentra nada devuelve nil. Por defecto la función string.find permite el uso de patrones de búsqueda, que son similares a las expresiones regulares, pero con limitaciones  y una sintaxis diferente.

> string.find("Tutoriales de Lua en dbtutoriales.com", "Lua")
15      17
> string.find("Tutoriales de Lua en dbtutoriales.com", "Luna")
nil

El parámetro opcional inicio, un entero, permite definir la posición a partir de la cual se quiere realizar la búsqueda. Este parámetro puede ser positivo o negativo, para definir si se quiere empezar por la izquierda (positivo) o por la derecha (negativo).

> string.find("Tutoriales de Lua en dbtutoriales.com", "Lua", 5)
15      17
> string.find("Tutoriales de Lua en dbtutoriales.com", "Lua", 20)
nil
> string.find("Tutoriales de Lua en dbtutoriales.com", "Lua", -25)
15      17

El cuarto parámetro opcional plano, un booleano, permite desactivar el uso de patrones de búsqueda, de este modo los caracteres especiales pasados en patrón, son evaluados como caracteres a buscar y no como caracteres de patrón de búsqueda.

> string.find("Tutoriales de Lua 100% en dbtutoriales.com", "%sL")
14      15
> string.find("Tutoriales de Lua 100% en dbtutoriales.com", "%sL", 1, true)
nil
> string.find("Tutoriales de Lua 100% en dbtutoriales.com", "%", 1, true)
22      22
> string.find("Tutoriales de Lua 100% en dbtutoriales.com", "%")
stdin:1: malformed pattern (ends with '%')
stack traceback:
        [C]: in function 'string.find'
        stdin:1: in main chunk
        [C]: in ?

string.gsub

string.gsub(cadena, patrón, reemplazo [, n)

Devuelve una copia de cadena en la que todas las ocurrencias de patrón han sido reemplazadas por reemplazo. Si se especifica un número en el cuarto parámetro n, se reemplazarán las n ocurrencias de patrón en cadena. Esta función devuelve un segundo valor que representa el número de ocurrencias reemplazadas.

> string.gsub("manzana, piña, plátano", "piña", "pera")
manzana, pera, plátano   1
> string.gsub("manzana, piña, plátano, melón, piña", "piña", "pera")
manzana, pera, plátano, melón, pera     2
> string.gsub("manzana, piña, plátano, melón, piña", "piña", "pera", 1)
manzana, pera, plátano, melón, piña     1

string.format

string.format(formato, arg1, arg2, ..., argn)

Crea una cadena formateada a partir del formato y argumentos proporcionados. formato acepta un número arbitrario de marcadores de formato. El número de argumentos pasados debe coincidir con el número de marcadores pasados. Esta función es similar a la función sprintf() de C.

Los marcadores s y q aceptan valores string. Los marcadores A, a, E, e, f, G y g aceptan valores number. Los marcadores c, d, i, o, u, X y x aceptan valores integer.

> string.format("%c%c%c", 76, 117, 97)                  -- Char (caracter)
Lua
> string.format("%e %E", 2.71828, 2.71828)              -- Exponent (exponente)
2.718280e+000 2.718280E+000
> string.format("%f", 2.71828)                          -- Float (coma flotante)
2.718280
> string.format("%g %g", 2.71828, 10e5)                 -- Float o exponent (según tipo usa uno u otro)
2.71828 1.000000e+006
> string.format("%d %i %u", -10, -10, -10)              -- Signed, signed, unsigned integer (enteros con o sin signo)
-10 -10 18446744073709551606
> string.format("%s %s %q", "Tutoriales", "de", "Lua")  -- String, quoted (cadenas y entre comillas)
Tutoriales de "Lua"
> string.format("%o %x %X", 255, 255, 255)              -- Octal, hexadecimal, hexadecimal
377 ff FF
> string.format("%a %A", 255, 255)                      -- Hexadecimal con exponente binario (>= Lua 5.2)
0x1.fep+7 0X1.FEP+7

El marcador q pone la cadena pasada entre comillas usando caracteres de escape, de tal manera que la cadena devuelta puede ser evaluada de nuevo por el intérprete de Lua sin producir errores.

> string.format("%s %q %s", "Aprendiendo", "Lua", "con dbtutoriales.com")
Aprendiendo "Lua" con dbtutoriales.com

string.upper

string.upper(cadena)

Devuelve una copia de cadena en la que todas sus letras han sido convertidas a mayúsculas.

> string.upper("Tutoriales de Lua en dbtutoriales.com")
TUTORIALES DE LUA EN DBTUTORIALES.COM

string.lower

string.lower(cadena)

Devuelve una copia de cadena en la que todas sus letras han sido convertidas a minúsculas.

> string.lower("Tutoriales de Lua en dbtutoriales.com")
tutoriales de lua en dbtutoriales.com

string.reverse

string.reverse(cadena)

Devuelve una copia de cadena invertida

> string.reverse("Tutorial de Lua")
auL ed lairotuT

string.byte

string.byte(cadena [, inicio [, fin)

Devuelve el valor numérico de los caracteres comprendidos entre inicio y fin de cadena, ambos comprendidos, en la codificación ASCII. Si no se especifica inicio, se devuelve el valor del primer carácter. Se puede decir que es la función inversa de string.char().

> string.byte("Lua")
76
> string.byte("Lua", 2)
117
> string.byte("Lenguaje Lua", 5, 10)            -- Devuelve los carácteres comprendidos entre 5 y 10 ambos incluidos, total 6 carácteres
117     97      106     101     32      76
> string.byte(" ")
32

string.char

string.char(i1, i2, ... in)

Devuelve una cadena representando los valores numéricos de carácter ASCII pasados como argumentos. Se puede decir que es la operación inversa de la función string.byte();

> string.char(76, 117, 97)
Lua

string.len

string.len(cadena)

Devuelve la longitud de la cadena.

> string.len("Tutoriales de Lua")
17

string.rep

string.rep(cadena, n)

Devuelve una cadena en la que cadena aparece concatenado n veces.

> string.rep("Lua ", 3)
Lua Lua Lua
> string.rep("Lua\n", 3)
Lua
Lua
Lua

Estructuras de control de flujo

Las estructuras de control son unas construcciones del lenguaje que permiten el control del flujo de la aplicación. Para ello se aplican una serie de condiciones que irán dirigiendo el flujo de ejecución. Las estructuras de control se dividen en bloques, que definen una o varias condiciones que serán evaluadas. Si el resultado de la evaluación es verdadero (true) se ejecuta el código definido en el bloque, si no, se continúa con la evaluación del resto de los bloques si los hubiera. Si ninguna condición es verdadera, se ejecutaría el código del bloque por defecto, solamente si este ha sido definido.

Una estructura de control se define usando los bloques if, elseif y else.


Bloque if

Las estructuras de control deben definir al menos una condición que se construye siempre con la palabra clave if:

if (expresion) then
    print("Verdadero")
end

La expresión a evaluar se define dentro de los paréntesis:

a = 5

if (a > 1) then
    print("a es mayor que 1")
end

Si ejecutas el código anterior te devolverá:

$ lua TestEstructuras.lua
a es mayor que 1

Bloque else

En ocasiones se necesita un bloque adicional que extienda a un bloque if y que permita dar una opción alternativa si la condición no se cumple. Este bloque, que es opcional, se define con la palabra else. Un bloque else se ejecutará si la condición del bloque if no se cumple:

a = 5

if (a > 5) then
    print("a es mayor que 5")
else
    print("a es menor o igual que 5")
end

Al ejecutar el ejemplo anterior verás que el bloque que se ejecuta es el bloque else, ya que, a = 5 y no cumple con la condición a > 5, con lo que obtenemos:

$ lua TestEstructuras.lua
a es menor o igual que 5

Bloque elseif

El bloque elseif permite extender al bloque if con condiciones adicionales. Su uso es opcional y pueden encadenarse tantos bloques como condiciones se necesiten. El bloque if siempre debe ir primero y a continuación se irán añadiendo todos los bloques elseif. Si es necesario, podemos añadir también un bloque else, pero este tendrá que posicionarse obligatoriamente al final, siendo el último bloque.

a = 3

if (a <= 1) then
    print("a es igual o menor que 1")
elseif (a == 2) then
    print("a es igual a 2")
elseif (a == 3) then
    print("a es igual a 3")
else
    print("a es mayor o igual que 5")
end

Al ejecutar el ejemplo anterior se tiene:

$ lua TestEstructuras.lua
a es igual a 3

Prueba el mismo ejemplo dándole a la variable a distintos valores para que puedas comprender mejor como funciona.

Bucles

En ocasiones necesitamos hacer algunas tareas repetitivas y necesitamos que el programa ejecute una serie de instrucciones repetidamente. Para este tipo de necesidad existen los bucles, que ejecutan una serie de instrucciones de manera repetida hasta que se detengan en base a una condición. Esta condición debe cumplirse para que empiecen a ejecutarse y seguirán haciéndolo mientras esta condición sea válida. En el momento en que la condición ya no sea válida el bucle se detiene.

Los ciclos de funcionamiento de un bucle se llaman iteraciones. Una iteración empieza con la verificación de la condición y si esta es true, se ejecuta el bloque de código. Las iteraciones se repiten hasta que la condición sea false. Sin embargo, existe una excepción que veremos a continuación.

Lua ofrece los siguientes tipos de bucles:

Tipo de bucle Descripción
while El bucle while define una condición y un bloque de código
for El bucle for define una secuencia y un bloque de código
repeat El bucle repeat es similar al bucle while pero invertido

Todos los bucles en Lua se pueden anidar, esto es, que pueden ponerse bucles dentro de otros bucles, para construir así operaciones más complejas.


Bucle while

El bucle while es el más polivalente de todos los bucles. Acepta una condición de cualquier tipo permitiendo mucha flexibilidad a la hora de controlarlo. Este tipo de bucle se puede ejecutar cero o más veces.

La sintaxis de un bucle while es:

while(condition)
do
    -- Instrucciones
end

A continuación se muestra un ejemplo de bucle while:

i = 1

while(i <= 10)
do
    print(i)
    i = i + 1
end

Que produce la siguiente salida:

1
2
3
4
5
6
7
8
9
10

Bucle for

El bucle for está especializado en la iteración de tablas y secuencias. Permite principalmente definir el número de veces que el bucle debe ejecutarse. El bucle for puede ejecutarse cero o más veces.

La sintaxis del bucle for es:

for inicializacion, objetivo, incremento
do
    -- Instrucciones
end

Veamos en más detalle los tres componentes que debemos definir en el bucle for:

En el bucle for, cuando se ejecuta la primera iteración, se realiza la inicialización de la variable de control. Luego se verifica que la variable de control no es igual al valor máximo/mínimo y si es diferente se ejecutan las instrucciones. Al finalizar el bloque de instrucciones, se incrementa la variable de control con el valor definido en incremento. En las siguientes iteraciones, se vuelve a comprobar la variable de control con el objetivo, si aun no ha sido alcanzado, se vuelven a ejecutar las instrucciones y se vuelve a incrementar la variable de control. Estas iteraciones se repetirán hasta que la variable de control tenga el mismo valor que el objetivo definido. Todas estas operaciones son realizadas por el intérprete automáticamente.

En el siguiente ejemplo se imprime una lista de números del 1 al 10:

i = 1
for i = 1, 10, 1
do
    print(i)
end

El resultado del ejemplo anterior es:

1
2
3
4
5
6
7
8
9
10

Bucle repeat

A diferencia de los bucles while o for, el bucle repeat...until ejecuta el bloque de instrucciones al menos una vez, siempre antes de verificar la condición. Una vez ejecutado el bloque de instrucciones se verifica la condición, si esta es false, se vuelve a ejecutar el bloque de instrucciones, si es true, la ejecución finaliza.

El bucle repeat...until es similar al bucle while, la única diferencia es que el bucle repeat...until verifica la condición después de ejecutar el bloque de código.

La sintaxis del bucle repeat...until es:

repeat
    -- Instrucciones
until(condiciones)

Como puedes observar en el ejemplo anterior, la expresión condicional se encuentra después del bloque de instrucciones, de este modo el bloque de instrucciones siempre se ejecuta al menos una vez antes de que se verifique la condición.

Un ejemplo de uso del bucle repeat...until es el siguiente:

mensajes = {}

repeat
  if (#(mensajes) == 0) then
      print("No hay mensajes")
  else
      print(table.remove(mensajes, 1))
  end
until(#(mensajes) == 0)

El resultado del ejemplo anterior es:

No hay mensajes

Como ves, el bloque de código se ha ejecutado una sola vez, ya que la tabla mensajes está vacía. Probemos ahora con algunos mensajes:

mensajes = {
  "Mensaje 1",
  "Mensaje 2",
  "Mensaje 3"
}

repeat
  if (#(mensajes) == 0) then
    print("No hay mensajes")    
  else        
    print(table.remove(mensajes, 1))
  end
until(#(mensajes) == 0)

Cuyo resultado es:

Mensaje 1
Mensaje 2
Mensaje 3

En este caso ya no aparece No hay mensajes. Al ejecutarse el bloque de instrucciones mensajes tiene una longitud de 3, con lo que se extrae el primer elemento de la lista y se imprime (Mensaje 1). Durante la verificación de la condición, esta no se cumple porque mensajes tiene una longitud de 2. El bloque se ejecuta una vez más, extrayendo el primer elemento de la lista. Al comprobar la condición, mensajes tiene una longitud de 1, con lo que no se cumple y la ejecución continua. El bloque se ejecuta de nuevo, extrae el primer elemento de la lista y lo imprime. Durante la verificación de la condición, esta vez mensajes tiene una longitud de 0, por lo que el bucle finaliza.

Bucles anidados

El anidamiento de bucles permite la realización de tareas complejas, como por ejemplo, el recorrido de arrays multidimensionales. Cuando anidamos bucles, lo podemos hacer con cualquier tipo de bucle, pudiendo mezclarlos si es preciso.

A continuación se muestran algunos ejemplos:

-- Bucle while anidado en un bucle for

for indice, valor, incremento
do
  while(condicion)
  do
    -- instrucciones
  end
  
  -- instrucciones
end
-- Bucle while anidado en bucle while

while(condicion)
do
  while(condicion)
  do
    -- instrucciones
  end
  
  -- instrucciones
end
-- Bucle for anidado en otro bucle for

for indice, valor, incremento
do
  for indice, valor, incremento
  do
    -- instrucciones
  end
  -- instrucciones
end

Instrucción break

La instrucción break permite controlar la ejecución del bucle. Con ella podemos finalizar el bucle en todo momento, sin que sea necesario que la condición se cumpla.

Cuando el intérprete encuentra una instrucción break dentro de un bucle, sale del bucle y continua ejecutando las instrucciones siguientes. Si el la instrucción break se encuentra dentro de un bucle anidado, la ejecución pasará al bucle padre.

El siguiente ejemplo muestra el funcionamiento de la instrucción break:

print("Antes del bucle")

for i = 1, 5, 1
do
  if (i > 3) then
    break
  end

  print("-> " .. i)

  for j = 1, 5, 1
  do
    if (j > 3) then
      break
    end

    print("   |- " .. j)
  end
end

print("Despues del bucle")

Al ejecutar el código anterior obtenemos el siguiente resultado:

Antes del bucle
--> 
|- 1
|- 2
|- 3
--> 2
|- 1
|- 2
|- 3
--> 3
|- 1
|- 2
|- 3
Despues del bucle

Observa como el uso de la instrucción break nos ha permitido modificar el funcionamiento del bucle. En lugar de imprimir hasta el número 5, que es el máximo en ambos bucles, hemos puesto una condición que sale del bucle cuando llega a 3. Cuando esto sucede en el bucle anidado, la ejecución continua en el bucle padre. Cuando el bucle padre llega a 3, finaliza y continua con la última instrucción del ejemplo.

Bucle infinito

Un bucle es infinito cuando la condición es siempre válida. Sirve generalmente para casos en los que se necesita que el bucle se ejecute continuamente como base del funcionamiento del programa. Para ello se usa generalmente el bucle while al que le ponemos como condición la palabra clave true, de este modo forzamos que la condición sea siempre válida. Un ejemplo de bucle infinito es:

while(true)
do
 -- instrucciones
end

Siempre puedes finalizar el bucle en todo momento con la instrucción break.

Funciones

La funciones son unas estructuras del lenguaje que permiten agrupar una serie de instrucciones que realizan una tarea específica. Esta estructura puede recibir nombres diversos, como por ejemplo: métodos, subrutinas o procedimientos. Al igual que en muchos otros lenguajes de programación, las funciones en Lua, permiten la abstracción y reutilización de fragmentos de código. En un programa en Lua se pueden definir tantas funciones como sean necesarias y sin restricción alguna sobre cuál es su tarea específica. 

Las funciones pueden ser definidas por parte del usuario, pero también pueden estar definidas en el propio lenguaje de programación. Un ejemplo típico sería la función print(), que forma parte del propio lenguaje Lua y que permite escribir un valor en la consola.


Definición de una función

La notación [...] significa que los elementos entre corchetes son opcionales. En caso de usarse, se deben escribir sin los corchetes.

La sintáxis de una función en Lua es:

[local] function nombre_funcion ([argument1, argument2, ..., argumentn])
  -- instrucciones
  [return variable1, variable2, ..., variablen]
end

Las funciones en Lua pueden aceptar valores de entrada, conocidos como argumentos y opcionalmente devolver un valor. Tanto los argumentos son opcionales como lo es también la devolución de un valor. El uso de los argumentos y la devolución de un valor estarán condicionados a la tarea específica que tenga la función. Veamos con más detalle los componentes:

Ejemplos de funciones en Lua

A continuación se muestran dos ejemplos simples de funciones que realizan la suma de dos números. En el primero la función usa variables globales tanto para obtener los datos como para registrar el resultado. En el segundo ejemplo, la función acepta dos argumentos, los operandos a y b que serán usados para realizar la operación, que sera devuelta usando la palabra clave return.

a = 5
b = 3
c = 0

function suma_numeros()
  c = a + b
end

suma_numeros()

print("El resultado de la suma de a + b es: " .. c)

A las variables ab se les asignan los valores 5 y 3. La función se define sin argumentos, por lo que, se usan directamente los valores de las variables. Dentro de la función se realiza la suma cuyo resultado se asigna a la variable c. Como puedes observar, en la función no se define tampoco ningún valor de retorno. Para ejecutar la función debemos invocarla, que corresponde con la instrucción suma_numeros().  El resultado del código anterior es:

$lua funcion_basica_sumar_1.lua
El resultado de la suma de a + b es: 8

En el resultado se observa que la suma de a y b se ha asignado a la variable c, cuyo valor después de invocar a la función es 8. Vamos a ver ahora otro ejemplo pasando argumentos y usando el retorno de valores:

va = 5
vb = 3

function suma_numeros(a, b)
  return "   El valor de la suma a + b es: " .. a + b
end

print("Suma pasando dos variables como argumentos:")
print(suma_numeros(va, vb))
print("Suma pasando dos literales a la funcion:" )
print(suma_numeros(6, 5))

En el ejemplo anterior, se invoca la función suma_numeros de dos formas distintas: una pasando las variables va y vb como argumentos; la otra, pasando los argumentos como valores literales. El resultado devuelto por la función, puede ser impreso directamente en la consola usando la instrucción print(). El resultado es:

$ lua funcion_basica_sumar_2.lua
Suma pasando dos variables como argumentos:
   El valor de la suma a + b es: 8
Suma pasando dos literales a la funcion:
   El valor de la suma a + b es: 11

Argumentos de una función

Los argumentos en las funciones son opcionales. Si se definen, no existe un límite en cuanto a su número. El conjunto de argumentos que se definan se conocen con el nombre de parámetros formales. 

Cada argumento definido, debe recibir un nombre único. Cada uno de estos argumentos constituye una variable cuyo ámbito es local a la función, esto es, solo se pueden acceder desde las instrucciones en el cuerpo de la función. El ciclo de vida de estas variables empieza con la asignación de valores durante la invocación y termina cuando se sale de la función, ya sea por llegar a una instrucción return, break o bien porque se han ejecutado todas las instrucciones. Al terminar la ejecución de la función los argumento son eliminados y ya no serán accesibles.


Llamada a una función

Las funciones son definidas primero y ejecutadas después en aquellas partes del código donde sean necesarias. Para hacer uso de las funciones que han sido definidas, hay que invocarlas.

Cuando se invoca a una función, se pasarán los valores de los argumentos, si se hubieran definido. Los valores de los argumentos pueden ser definidos de diversas formas, como por ejemplo: variables, literales, valores retornados por funciones, etc. 

Si la función retorna un valor, este puede ser asignado a una variable, una tabla, asignado como parámetro de otra función o incluso ignorarlo si no lo necesitamos. 

En el siguiente ejemplo se muestra una combinación de paso de argumentos por variable y por resultado de función:

va = 5
vb = 3
vc = 6
vd = 4

function suma_numeros(a, b)
  return a + b
end

print("Suma pasando el resultado de dos funciones:")
resultado_a = suma_numeros(suma_numeros(va, vb), suma_numeros(vc, vd))
print("   El resultado de la suma de va + vb + vc + vd es: " .. resultado_a)

En el ejemplo anterior podemos ver que se han anidado las llamadas a la función suma_numeros, usando el valor devuelto por las llamadas anidadas para los argumentos de la llamada madre. Esto es posible, porque el intérprete de Lua, irá ejecutando las funciones desde la más profunda a la más externa, asegurando que todos los valores estarán disponibles. En el siguiente diagrama se muestra el orden en el que serán ejecutados los componentes:

El resultado que obtenemos al ejecutar el código de ejemplo anterior es:

$ lua funcion_basica_sumar_3.lua
Suma pasando el resultado de dos funciones:
   El resultado de la suma de va + vb + vc + vd es: 18

Asignación y paso de funciones

En el apartado anterior hemos presentado el paso de una función como argumento para otra. Este es uno de los métodos de asignación de funciones. A parte de este una función se puede asignar a una variable y también a una tabla.  Observa el ejemplo a continuación:

-- Función para imprimir el resultado por consola
function mostrar_resultado(resultado)
    print("El resultado es: ", resultado)
end

-- Función anónima que es asignada a la variable sumar
sumar = function (a, b, funcion_mostrar)
    c = a + b
    funcion_mostrar(c)
end

mostrar_resultado(6)
sumar(5, 3, mostrar_resultado)

La primera función es una función común, que acepta un solo argumento y que no retorna ningún valor. La segunda función es asignada a una variable, sin darle ningún nombre, conocida como función anónima. Este tipo de definición permite que una variable porte la referencia a la función. Cuando se define una función de este modo, solo se podrá llamar a esta función usando la variable, como se muestra en la línea 13. También en la llamada de la línea 13, podemos observar que hemos pasado el nombre de la función mostrar_resultado como argumento, para que, posteriormente se pueda llamar desde dentro de la función a ésta función pasada como argumento (línea 9).


Función con argumentos variables

En ocasiones puede presentarse la necesidad de definir una función que admita un número de argumentos variable que no puede ser determinado cuando se define la función. Para este tipo de casos Lua ofrece las funciones con argumentos variables, en las que la definición de los argumentos se realiza usando tres puntos consecutivos: .... Veamos un ejemplo para comprender mejor de qué se trata.

function suma(...)
    local suma = 0
    local argumentos = {...}

    for i,v in ipairs(argumentos) do
        suma = suma + v
    end

    return suma
end


print("La suma de los numeros 1,3,8,11,16 es: " .. suma(1,3,8,11,16))

Cuando se ejecuta el código anterior se obtiene el siguiente resultado:

$ lua funcion_basica_sumar_5.lua
La suma de los numeros 1,3,8,11,16 es: 39

Estructuras de datos

Estructuras de datos

Tablas

Lua implementa un tipo de base con el que construir diferentes tipos de estructuras de datos. Se llaman tablas y ofrecen las características necesarias para la creación de estructuras de datos complejas que pueden ser manejadas de una manera eficiente.

En este capítulo vamos a ver qué es una tabla en Lua y diferentes tipos de estructuras de datos que pueden ser implementadas usando las tablas, para disponer de estructuras específicas para casos particulares, como arrays o diccionarios.


Tablas

Las tablas, tables por su denominación en inglés, en Lua son colecciones de pares clave/valor, cuyos valores pueden ser de cualquier tipo excepto nil.  Constituyen la única estructura de datos disponible en Lua y sirve de base para la creación de otras estructuras de datos.  Las tablas se indexan por la clave, así, se puede recuperar cualquier valor de la tabla si conocemos la clave. Dado que las claves y valores de las tablas en Lua pueden ser de cualquier tipo, una clave o un valor pueden ser a su vez de tipo tabla. Las tablas en Lua son dinámicas: no tienen un tamaño definido y pueden crecer según las necesidades.

A partir de una tabla podemos construir cualquier tipo de estructura de datos más compleja o específica, como por ejemplo arrays, arrays multidimensionales, matrices, listas, colas, etc. 

Para facilitar la manipulación de las tablas, Lua dispone de una librería estándar que define una serie de funciones para operar con las tablas. 

Sintaxis de las tablas

Las tablas se definen por medio de los constructores de tablas, que se representan por los caracteres de llaves { y }.  Una tabla vacía se define como a continuación.

> tabla = {}
> print(tabla)
table: 00000000007d4230

En el ejemplo anterior el valor retornado por la función print es el tipo y el identificador del objeto. Para poder recuperar un valor contenido en la tabla debemos hacer referencia a un elemento por su clave.

Uso de las tablas

La tabla es un tipo complejo y flexible que permite la gestión de estructuras complejas de datos. A continuación tratamos las diferentes posibilidades de uso.

Definición y asignación

Una vez definida una tabla, podremos acceder a cada uno de sus elementos a través de la clave. Para insertar valores en una tabla,  damos valor a la clave y le asignamos un valor.

> tabla = {}
> tabla["clave"] = "Lua"      -- Insertamos con la clave 'clave' el valor 'Lua', ambos cadenas
> tabla[1] = 1000             -- Insertamos con la clave '1' el valor '1000', ambos enteros
> = tabla["clave"]            -- Recuperamos el valor de la clave 'clave'
Lua
> = tabla[1]                  -- Recuperamos el valor de la clave '1'
1000

Como ves en el ejemplo anterior, las claves pueden ser de cualquier tipo, pudiéndose incluso mezclar. Si bien, por lo general, en muchos tipos de estructuras de datos las claves suelen ser del mismo tipo. Del mismo modo, los tipos de la clave y el valor pueden ser diferentes también. En todos los casos la clave nunca puede ser nil ni NaN (not a number).

> tabla = {}
> clave = {}
> funcion = function () end
> tabla[clave] = 250            -- Asignamos 250 a la clave 'clave' que es una tabla
> tabla[funcion] = 500          -- Asignamos 500 a la clave 'funcion' que es una funcion
> = tabla[clave]                -- Recuperamos el valor de la clave 'clave'
250
> = tabla[funcion]              -- Recuperamos el valor de la clave 'funcion'
500
> tabla[nil] = 125              -- Asignar un valor a una clave 'nil' produce un error
stdin:1: table index is nil
stack traceback:
        stdin:1: in main chunk
        [C]: in ?
> tabla[0/0] = 450              -- Asignar un valor a una clave 'NaN' también produce error
stdin:1: table index is NaN
stack traceback:
        stdin:1: in main chunk
        [C]: in ?

Cuando se dice que una clave puede ser de cualquier tipo, significa también que una clave puede ser el resultado de una expresión válida cualquiera.

> tabla = {}
> tabla[1/0] = 750                                 -- Clave basada en una operación aritmética
> tabla[0/1] = 1000                                -- Otro ejemplo de operación aritmética
> = tabla[1/0]
750
> = tabla[0/1]
1000
> tabla[10^3] = 1250                               -- Operación aritmética: exponenciación
> = tabla[10^3]
1250
> tabla[string.gsub("aa", "a", "b", 1)] = 1500     -- Clave basada en el retorno de la llamada a una función
> = tabla[string.gsub("aa", "a", "b", 1)]
1500
Acceso a claves no definidas

En el caso de que recuperemos el valor de una clave que no existe, no se produce ningún error y devuelve nil.

> tabla = {}
> = tabla[100]
nil
Supresión de pares

Cualquier clave que hayamos declarado puede ser eliminada asignándole el valor nil.

> tabla = {}
> tabla["clave"] = 10
> = tabla["clave"]
10
> tabla["clave"] = nil
> = tabla["clave"]
nil
Acceso simplificado a claves tipo string

Las tablas también ofrecen un modo de acceso sencillo para las claves de tipo string, reduciendo el código necesario para acceder a una clave. Los únicos requisitos para este tipo de claves es respetar que sean de tipo string y que tengan la siguiente sintaxis:

Una cadena que contenga letras, números y guión bajo ( _ underscore). No puede comenzar por un número.

> tabla = {}
> tabla.clave = 1000
> = tabla.clave
1000
> tabla.1 = 500
stdin:1: syntax error near '.1'
> tabla._1 = 500
> = tabla._1
500
Definición y asignación avanzada

Hasta ahora hemos visto una forma para la definición de una tabla: primero declaramos la tabla y luego le asignamos los valores. Sin embargo Lua ofrece la posibilidad de declarar e inicializar una tabla en una sola expresión. Veamos un ejemplo de definición y asignación en una sola sentencia.

tabla = {["clave"] = "valor", [123] = 1000}
= tabla.clave
valor
= tabla["clave"]
valor
= tabla[123]
1000

En el ejemplo anterior, puedes observar que en la primera línea, a la variable tabla, se le asigna una tabla en la cual los pares clave valor le son definidos en la misma línea.

Estructuras de datos

Arrays

En el capítulo anterior hemos visto en detalle las tablas, que son la estructura de datos básica de Lua. Las tablas son

 extremadamente flexibles y permiten crear tipos específicos de estructuras de datos. En este nuevo capítulo vamos a ver con más detalle un tipo específico de tablas: los arrays.

Los arrays son estructuras de datos indexadas, formadas por una lista de pares de elementos, cada elemento tiene una clave numérica y un valor que puede ser de cualquier tipo, incluyendo otros arrays. Por lo tanto un array en Lua no es más que una tabla en la que las claves son numéricas.

Como las tablas, los arrays no tienen una longitud fija y pueden albergar tantos datos como los recursos disponibles lo permitan.

Arrays unidimensionales

A diferencia de otros lenguajes, en Lua, los índices empiezan en 1 y no en 0. Por ello diremos que los arrays en Lua son de índice 1.

Los arrays unidimensionales representan una lista de un solo nivel. Se pueden crear en Lua definiendo una tabla simple:

ar = {"elemento 1", "elemento 2", "elemento 3"}

for i = 1, 3 do
  print(i, "-", ar[i])
end

En el ejemplo anterior, hemos definido una tabla con tres elementos de tipo cadena. Automáticamente, como no hemos definido una clave para cada uno de ellos, Lua le asigna una clave numérica incremental. Así elemento 1 tendrá el índice 1 y elemento 3 tendrá índice 3. Cuando ejecutos el código anterior tenemos la siguiente salida:

1       -       elemento 1
2       -       elemento 2
3       -       elemento 3

Si un índice de un array no existe, cuando intentamos accederlo, Lua devuelve nil:

> ar = {"elemento 1", "elemento 2", "elemento 3"}
> print (ar[4])
nil

Un array también puede ser definido como una tabla vacía e inicializarlo usando un bucle, por ejemplo:

ar = {}

for i = 1, 10 do
  ar[i] = i
end

for i = 1, 10 do
  print(ar[i])
end

El ejemplo anterior produce la siguiente salida:

1
2
3
4
5
6
7
8
9
10
>

Array multidimensional

Los arrays multidimensionales son aquellos que tienen otros arrays anidados.  Un ejemplo de array multidimensional se muestra a continuación:

> md = {{"elemento 1.1", "elemento 1.2"}, {"elemento 2.1", "elemento 2.2"}}
> print(md[1][2])
elemento 1.2

En la primera línea del ejemplo definimos un array de dos elementos cuyos valores son dos arrays de dos elementos cada uno. Para acceder al los elementos de cada array hay que poner grupos de corchetes según los niveles que queremos acceder. En nuestro ejemplo md[1][2], el primer grupo de corchetes por la izquierda [1] representa al array padre, que contiene dos arrays; el segundo grupo [2], representa a un elemento dentro del array hijo. Por ello md[1][2], representa al elemento elemento 1.2, que corresponde con el segundo elemento del primer array.

Los arrays multidimensionales se pueden recorrer anidando los bucles también. Primeramente tendremos un bucle que recorerá el array padre y luego otro bucle anidado que recorrerá el array hijo. Observa el siguiente ejemplo:

 md = {{"elemento 1.1", "elemento 1.2"}, {"elemento 2.1", "elemento 2.2"}}

for i = 1, 2 do
  for j = 1, 2 do
    print(i, j, md[i][j])
  end
end

Al ejecutar el código anterior se produce la siguiente salida:

1       1       elemento 1.1
1       2       elemento 1.2
2       1       elemento 2.1
2       2       elemento 2.2

Hasta ahora se ha presentado el caso de un array bidimensional, pero, no hay limitación en cuanto a los niveles de un array multidimensional.

Longitud de un array

Hasta ahora hemos mostrado en todos los ejemplos el valor constante de un array para recorrerlos con un bucle. En la mayoría de aplicaciones prácticas donde existan arrays de longitud variable, este método no es muy práctico. 

El operador # de Lua permite recuperar la longitud de un array. Ahora, podemos reescribir uno de los ejemplos anteriores para que sea más dinámico y no tan rígido:

ar = {}

for i = 1, 10 do
  ar[i] = i
end

ar[#ar + 1] = #ar + 1

for i = 1, #ar do
  print(ar[i])
end

En el ejemplo anterior, inicializamos un array con 10 elementos del 1 al 10. En la siguiente línea usamos el operador # para introducir en la posición # + 1 el valor # + 1, que corresponde con 11.  De este modo podemos alterar de forma dinámica nuestro array, sin importar si tiene 5, 20 o 1000 elementos.  Ahora cuando ejecutamos el código anterior obtenemos el siguiente resultado:

1
2
3
4
5
6
7
8
9
10
11

Asignación de valores en un array

Una vez inicializados los arrays, sus valores pueden ser modificados en todo momento. Observa los siguientes ejemplos:

ar = {"elemento 1", "elemento 2", {"elemento 3.1", {"elemento 3.2.1", "elemento 3.2.2"}, "elemento 3.3"}}

ar[2] = "modificado 2"
ar[3][2][1] = "modificado 3.2.1"
ar[3][3] = "modificado 3.3" 

print(ar[2])
print(ar[3][2][1])
print(ar[3][3])

Al ejecutar el código anterior obtenemos:

modificado 2
modificado 3.2.1
modificado 3.3

 

Estructuras de datos

Manipulación de tablas

Lua implementa funciones que permiten la manipulación de las tablas facilitando la realización de operaciones con ellas. Todas ellas están definidas en el módulo table de la líbrería estándar de Lua. Vamos a ver a continuación más en detalle estas funciones:


Función table.concat

La función concat permite la concatenación de elementos de una tabla. Todos los elementos de la tabla deben ser cadenas  o números. Veamos la signatura del método:

table.concat(lista, [, sep [, i [, j]]])

Detalle de los argumentos:

lista. Lista que se quiere concatenar, todos sus elementos deben ser cadenas o números.
sep. Separador, opcional. Es el caracter que se usará como separador. Si no se especifica se usa por defecto la cadena vacía.
i. Indice de inicio, opcional. Permite definir el índice del primer elemento  que se concatenará. Por defecto es 1.
j. Indice de fin, opcional. Permite definir el índice del último elemento que se concatenará. Por defecto es #lista.

Veamos ahora un ejemplo del uso de la función concat:

paises = {"Suiza", "Espana", "Colombia", "Italia", "Francia"}

-- Concatenación de la lista con los parámetros por defecto
print ("Concatenacion con parametros por defecto: ", table.concat(paises))

-- Concatenación con un caracter separador
print ("Concatenacion con un separador: ", table.concat(paises, "; "))

-- Concatenación de una sublista
print ("Concatenacion de una sublista: ", table.concat(paises, "; ", 2, 4))

Al ejecutar él código anterior, obtenemos el siguiente resultado:

Concatenacion con parametros por defecto:       SuizaEspanaColombiaItaliaFrancia
Concatenacion con un separador:         Suiza; Espana; Colombia; Italia; Francia
Concatenacion de una sublista:  Espana; Colombia; Italia

En la primera línea, los elementos de la lista se han concatenado sin separación, ya que por defecto, se usa una cadena vacía. En la segunda, se ha definido un separador. En la tercera se han definido, además del separador, el elemento de inicio y fin para la concatenación.

Función table.insert

Permite insertar un elemento en la tabla en la posición especificada, desplazando los demás elementos si es necesario.  La signatura de este método es como sigue:

table.insert(lista, [ pos,] valor)

Los argumentos son:

lista. Lista a la cual queremos insertarle un elemento.
pos. Posición, opcional. Define la posición donde queremos insertar el elemento dentro de la lista. Por defecto los elementos se insertal al final de la lista.
valor. Valor a insertar en la lista.

Veamos a continuación un ejemplo de uso de la función insert:

paises = {"Suiza", "Espana", "Colombia"}

-- Inserción de un elemento al final de la lista
table.insert (paises, "Cuba")
print ("Se agrego un elemento al final de la lista:", table.concat (paises, ", "))

-- Inserción de un elemento en el índice 3 de la lista
table.insert (paises, 3, "Venezuela")
print ("Se agrego un elemento en el indice 3 de la lista:", table.concat (paises, ", "))

Al ejecutar el código anterior obtenemos el siguiente resultado:

Se agrego un elemento al final de la lista:     Suiza, Espana, Colombia, Cuba
Se agrego un elemento en el indice 3 de la lista:       Suiza, Espana, Venezuela, Colombia, Cuba

En la primera línea, insertamos el elemento "Cuba" al final de la lista. En la segunda línea, insertamos el elemento "Venezuela" en la posición 3, como podemos ver, elemento que estaba en la posición 3, "Colombia", ha sido desplazado a la posición 4 y a su vez el elemento en la posición 4 ha sido desplazado a la posición 5.

Función table.remove

La función table.remove permite eliminar un elemento de la lista. Veamos la signatura del método:

table.remove (lista [, pos])

El detalle de los argumentos es:

lista. Lista de la cual eliminaremos el elemento.
pos. Posición, opcional. Posición que ocupa el elemento que queremos eliminar. Si no se especifica, se eliminará el último elemento de la lista.

Retorno: esta función retorna el elemento eliminado.

Veamos un ejemplo del uso de la función table.remove:

paises = {"Suiza", "Espana", "Colombia", "Italia", "Francia"}

-- Eliminamos el cuarto elemento de la lista
pais = table.remove (paises, 4)
print ("El pais eliminado de la lista es:", pais)
print ("Contenido de la lista:", table.concat (paises, ", "))

-- Vaciamos la lista
contenido = table.remove(paises)
print ("Paises eliminados de la lista:", contenido)
print ("Contenido de la lista:", table.concat(paises, ", "))

Al ejecutar el código anterior obtenemos el siguiente resultado:

El pais eliminado de la lista es:       Italia
Contenido de la lista:  Suiza, Espana, Colombia, Francia
Paises eliminados de la lista:   Francia
Contenido de la lista:  Suiza, Espana, Colombia

En la primera línea se ha eliminado "Italia" cuya posición es 4 dentro de la lista. En la segunda línea vemos los elementos restantes en la lista después de eliminar uno. En la tercera línea, no hemos especificado ningún elemento, por lo que se eliminará el último elemento de la lista, que es, "Francia".

Función table.move

La función table.move nos permite copiar elementos de una tabla a otra. Podemos copiar los elementos en una nueva tabla o bien copiarlos en la misma tabla. Esta función está disponible a partir de Lua 5.3. Esta es la signatura del método:

table.move (lista1, desde, hasta, insercion, [, lista2])

Veamos el detalle de los argumentos:

lista1. Lista desde donde copiaremos los elementos.
desde. Índice desde el cual comenzarán a copiarse elementos de la lista1.
hasta. Índice hasta el cual se copiarán elementos de la lista1.
insercion. Indice de la lista2 donde se insertarán los elementos copiados.
lista2. Opcional, lista donde se insertarán los elementos copiados. Si no se especifica una lista, el destino será lista1.

Retorno: devuelve lista2.

Vamos a ver un ejemplo de la función table.move:

paises = {"Suiza", "Espana", "Colombia", "Italia", "Francia"}
otros = {"Alemania"}

table.move (paises, 2, 3, 2, otros)

print ("Paises en tabla otros:", table.concat(otros, ", "))
print ("Paises en tabla paises:", table.concat(paises, ", "))

El resultado que obtendremos es el siguiente:

Paises en tabla otros:  Alemania, Espana, Colombia
Paises en tabla paises: Suiza, Espana, Colombia, Italia, Francia

Como puedes observar hemos copiado los elementos "Espana" y "Colombia" dentro de la nueva lista a continuación del elemento "Alemania", cuyo resultado es una lista con tres elementos. Por otro lado, vemos que la lista original conserva todos sus elementos.

Función table.sort

La función table.sort, permite ordenar los elementos  de una lista.  La signatura de la función es como sigue:

table.sort (lista [, comparador])

Veamos el detalle de los argumentos:

lista. Lista que queremos ordenar.
comparador. Opcional. Función que acepta dos elementos de la lista y que devuelve true si el primer elemento debe ir delante del segundo elemento. Si no se especifica, se usará el operador < de Lua.

Veamos un ejemplo:

paises = {"Suiza", "Espana", "Colombia", "Italia", "Francia"}

-- Ordenar los países por orden alfabético
table.sort (paises)
print ("Paises ordenados por orden alfabetico:", table.concat(paises, ", "))

-- Ordenar los países por orden alfabetico inverso
table.sort (paises, function (el1, el2) return el1 > el2 end)
print ("Paises ordenados por orden alfabetico inverso:", table.concat(paises, ", "))

function reordena (elementoA, elementoB)
    if elementoA < elementoB then
        return true
    else
        return false
    end
end

-- Ordenar las cifras usando una función externa
cifras = {23, 56, 11, 9, 32, 60, 3}

table.sort (cifras, reordena)
print ("Cifras ordenadas por funcion:", table.concat(cifras, ", "))

function inversa (elementoA, elementoB)
    if elementoA > elementoB then
        return true
    else
        return false
    end
end

-- Ordenar las cifras usando una función externa inversa
table.sort (cifras, inversa)
print ("Cifras ordenadas por funcion inversa:", table.concat(cifras, ", "))

Al ejecutar el ejemplo anterior, obtendremos el siguiente resultado:

Paises ordenados por orden alfabetico:  Colombia, Espana, Francia, Italia, Suiza
Paises ordenados por orden alfabetico inverso:  Suiza, Italia, Francia, Espana, Colombia
Cifras ordenadas por funcion:   3, 9, 11, 23, 32, 56, 60
Cifras ordenadas por funcion inversa:   60, 56, 32, 23, 11, 9, 3

Función table.unpack

La función table.unpack() devuelve los elementos de una lista. Su signatura es:

table.unpack (lista [,i [,j]])

Veamos el detalle de los argumentos:

lista. Lista de la cual se extraerán los elementos.
i. Opcional. Índice a partir del cual comenzar a extraer elementos. Su valor por defecto es 1.
j. Opcional. Índice del último elemento a extraer. Su valor por defecto es el último elemento de la lista (#lista)

Retorna: los elementos extraidos.

Veamos a continuación un ejemplo de la función table.unpack:

paises = {"Suiza", "Espana", "Colombia", "Italia", "Francia"}

espana, colombia = table.unpack (paises, 2, 3)

print ("Paises extraidos:", espana, colombia)

Cuyo resultado es:

Paises extraidos:       Espana  Colombia

Funcion table.pack

La función table.pack crea una nueva tabla a partir de un número arbitrario de elementos pasados como argumentos. Su signatura corresponde con:

table.pack (elm1 [, elm2 [, elm_n]])

elm1, elm2 ... elm_n. Elementos que serán insertados en la nueva tabla. 

Retorna: una nueva tabla con los elementos pasados.

Veamos un ejemplo de table.pack:

espana = "Espana"
colombia = "Colombia"
peru = "Peru"
argentina = "Argentina"
mexico = "Mexico"

paises = table.pack (espana, colombia, peru, argentina, mexico)

print ("Lista de paises:", table.concat(paises, ", "))

Si ejecutamos el código anterior obtendremos el siguiente resultado:

Lista de paises:        Espana, Colombia, Peru, Argentina, Mexico

 

Iteradores

Un iterador (iterator) en Lua es una estructura del lenguaje que permite recorrer los elementos de una tabla en el orden en el que están definidos. Los iteradores se presentan como una función, la cual, a cada iteración devuelve el siguiente elemento de la lista. El uso más común de un iterador es el de recorrer los elementos de una tabla dentro de un bucle para, por ejemplo, imprimir por la consola los valores.

El principio de funcionamiento de un iterador es simple: en un bucle for, el iterador es llamado a cada iteración. El iterador realiza la lógica necesaria y devuelve un valor. Cuando la tabla ha sido recorrida completamente el iterador devuelve nil y el bucle finaliza.

En Lua encontramos tres tipos de iteradores:

Veamos a continuación los diferentes tipos de iteradores que están disponibles en Lua.

Iterador genérico para bucles for

Los iteradores genéricos para bucles for están definidos en la librería estándar de Lua. Son dos funciones que extraen cada uno de los elementos de la tabla en forma de pares tipo clave/valor. La clave se corresponde con el índice/clave que ocupa el elemento en la tabla. Veamos a continuación un ejemplo:

paises = {"Suiza", "Espana", "Colombia", "Italia", "Francia"}

for clave,valor in ipairs(paises)
do
  print (clave, valor)
end

Al ejecutar el ejemplo anterior obtenemos el siguiente resultado:

1       Suiza
2       Espana
3       Colombia
4       Italia
5       Francia

Iteradores genéricos ipairs() y pairs()

En el ejemplo anterior, se ha presentado la funcion ipairs() que extrae los elementos en forma de índice/valor. ipairs() está orientada a las tablas de tipo array, como en el ejemplo anterior. Para cada elemento de la tabla, ipairs() nos devuelve el índice y su valor.

Existe otro iterador genérico, pairs(), que permite extraer los elementos de una tabla tipo diccionario. En esta estructura, la clave está definida con un valor no numérico. Veamos un ejemplo:

parametros = { ["host"] = "localhost", ["port"] = "5432", ["user"] = "postgres", ["database"] = "postgres" }

for clave, valor in pairs(parametros) do
    print(clave, valor)
end

El resultado del código anterior es:

database        postgres
port    5432
user    postgres
host    localhost

Como puedes observar, el valor devuelto en clave es la clave que ha sido definida para el elemento y, valor, devuelve el valor asignado a esa clave.

La función pairs() tiene algunas particularidades comparada con ipairs(). Por ejemplo, pairs() soporta solamente tablas de tipo hash (diccionarios). Si le pasamos un array no devuelve nada. Contrariamente si le pasamos un array a pairs() lo itera correctamente del mismo modo que lo hace ipairs(). Otro aspecto importante, es que pairs() no garantiza que la iteración de los elementos se produzca en el mismo orden en el que están definidos en la tabla. Por el contrario ipairs() sí garantiza que el orden de iteración corresponde con el orden dentro de la tabla. Veamos un ejemplo comparativo.

parametros = { ["host"] = "localhost", ["port"] = "5432", ["user"] = "postgres", ["database"] = "postgres" }
paises = {"Suiza", "Espana", "Colombia", "Italia", "Francia"}

print ("Iterando diccionario con pairs")

for clave, valor in pairs(parametros) do
    print(clave, valor)
end

print ("Iterando diccionario con ipairs")

for clave, valor in ipairs(parametros) do
    print(clave, valor)
end

print ("Iterando array con ipairs")

for clave, valor in ipairs(paises) do
    print(clave, valor)
end

print ("Iterando array con pairs")

for clave, valor in pairs(paises) do
    print(clave, valor)
end

Cuando ejecutamos el ejemplo obtenemos la siguiente salida:

Iterando diccionario con pairs
user    postgres
port    5432
database        postgres
host    localhost
Iterando diccionario con ipairs
Iterando array con ipairs
1       Suiza
2       Espana
3       Colombia
4       Italia
5       Francia
Iterando array con pairs
1       Suiza
2       Espana
3       Colombia
4       Italia
5       Francia

Vamos a analizar el resultado de la ejecución del código anterior por bloques.

Iterando diccionario con pairs
user    postgres
port    5432
database        postgres
host    localhost

En este primer bloque, la función pairs() ha iterado el diccionario que le hemos pasado, pero observa que los elementos han sido extraidos en un orden diferente al que tienen dentro de la tabla. El primer elemento de la tabla era {["host"] = "localhost"}, sin embargo, en la salida por consola aparece el último. Este comportamiento no es predecible y a cada ejecución el orden puede variar.

Iterando diccionario con ipairs

El segundo bloque, solo devuelve el título. Como explicábamos anteriormente, ipairs() no es capaz de iterar tablas de tipo hash, con lo cual no devuelve nada.

Iterando array con ipairs
1       Suiza
2       Espana
3       Colombia
4       Italia
5       Francia

En este tercer bloque estamos iterando un array con la función ipairs(). El resultado es el esperado: extrae de manera secuencial cada uno de los elementos en el orden de indexación.

Iterando array con pairs
1       Suiza
2       Espana
3       Colombia
4       Italia
5       Francia

En este cuarto y último bloque, estamos iterando un array con la función pairs(). Como puedes observar se comporta igual que ipairs(): todos los elementos han sido iterados de forma secuencial en el orden de indexación. Para el caso de arrays, pairs() sí tiene un comportamiento predecible y, para cada ejecución, producirá el mismo resultado.

Iteradores no genéricos

Hasta ahora hemos visto los iteradores genéricos que están definidos en la librería estándar de Lua. Estos iteradores son útiles para casos comunes y pueden servir para casi cualquier situación.

Sin embargo en casos particulares, los iteradores genéricos pueden no ser lo suficientemente específicos, es por ello que Lua soporta los iteradores definidos por el desarrollador.

En Lua podemos definir dos tipos de iteradores no genéricos: sin estado y con estado.

Iteradores no genéricos sin estado

Los iteradores no genéricos sin estado, tal como su nombre indica, no guardan su estado por si mismos. Esto significa que podemos definir un iterador y usarlo en varios bucles sin que la ejecución de uno afecte al otro.

El iterador se define como una función que recibe dos variables: un valor fijo y un valor variable, esta última se conoce como variable de control. La función debe devolver dos valores si la iteración tuvo éxito: el valor variable adaptado y el valor devuelto, que correspone con el elmento. Si no existieran más elementos a iterar la función debe devolver nil.

Veamos un ejemplo de iterador no genérico sin estado para entender mejor como funciona.

function suma (valorFijo, valorVariable)
    if (valorVariable < valorFijo)
    then
        valorVariable = valorVariable + 1
        return valorVariable, valorFijo + valorFijo * valorVariable
    end
end

for clave, valor in suma,10,0 do
    print(clave, valor)
end

En el ejemplo definimos una función suma(valorFijo, valorVariable) que acepta dos argumentos: valorFijo y valorVariable. Dentro de la función se verifica que valorVariable sea menor que valorFijo y si es verdadero, entonces incrementamos en una unidad valorVariable, devolviendo primero el nuevo valor de valorVariable y el resultado de sumar valorFijo a la multiplicación de valorFijo y valorVariable. En el caso que valorVariable sea mayor que valorFijo, entonces no devolvemos nada, que se interpreta como un valor nil.

El bucle lo hemos definido con una referencia a la función suma a la cual le pasamos como valorFijo el valor 10 y como valor inicial para valorVariable el valor 0.

Veamos el resultado de ejecutar este ejemplo.

1       20
2       30
3       40
4       50
5       60
6       70
7       80
8       90
9       100
10      110

Como puedes ver el iterador a ido incrementado los valores internamente, pero no mantiene su valor (no tiene estado). A cada iteración el bucle nos envía el último valor que le devolvimos para valorVariable. Observa la tabla de valores a continuación para entender mejor este concepto.

lua-iterator4.png

De este modo podemos concluir que en los iteradores sin estado, el que mantiene el estado es el propio bucle, ya que, envía de vuelta a la función el valor variable que recibió de ésta en la última llamada, por lo tanto, valorVariable constituye la variable de control de estado.

La función ipairs() es un ejemplo de iterador sin estado.

Iteradores no genéricos con estado complejo

En algunos casos, la variable de control no es suficiente para mantener el control de la iteración. En estos casos se puede usar una tabla donde se almacenarán los valores necesarios para el control de la iteración. La tabla se pasa como valor fijo a la función, ya que la tabla en si misma no cambia durante la ejecución, pero sí lo hace el contenido de la misma. Esta técnica permite mantener un número arbitrario de parámetros y valores para el control de la iteración.

Dado que todos los valores necesarios para el iterador están contenidos en la tabla, el uso de la variable de control resulta innecesario, con lo cual se puede omitir. Vamos a ver un ejemplo que nos permita entender mejor el funcionamiento de este tipo de iteradores. Vamos a reescribir el ejemplo del apartado anterior usando una tabla como método de control.

tablaEstado = {}

tablaEstado.valorFijo = 10
tablaEstado.valorVariable = 0
tablaEstado.elemento = 0
tablaEstado.indice = 0

function suma (tblEstado)
    if (tblEstado.valorVariable < tblEstado.valorFijo)
    then
        tblEstado.valorVariable = tblEstado.valorVariable + 1
        tblEstado.elemento = tblEstado.valorFijo + tblEstado.valorFijo * tblEstado.valorVariable
        tblEstado.indice = tblEstado.valorVariable
        return tblEstado
    end
end

for valor in suma,tablaEstado do
    print(valor.indice, valor.elemento)
end

Como puedes ver en el ejemplo, hemos definido una tabla hash (diccionario), con cuatro claves. Estas cuatro claves corresponden con las cuatro variables que habíamos usado en el ejemplo anterior. Dentro de la función suma hemos sustituido las variables originales por sus homólogas en la tabla. De este modo hemos hecho una refactorización del ejemplo anterior usando una tabla. En la función ahora, solamente devolvemos un valor, la tabla, que contiene todo lo necesario. En el bucle observa que solamente pasamos la referencia a la tabla, es decir pasamos la variable tablaEstado solamente, ya que como hemos dicho, ella contiene toda la información necesaria para el control. Al ejecutar el código anterior obtenemos el siguiente resultado.

1       20
2       30
3       40
4       50
5       60
6       70
7       80
8       90
9       100
10      110

Como puedes ver, es exactamente el mismo resultado que en el ejemplo anterior. Pero aun queda un detalle, en el bucle, observa que hemos eliminado la variable clave, esto es, porque como estamos devolviendo un solo valor, ya no tenemos el valor del índice, como en el caso anterior. Ahora lo que hacemos es definirlo esplícitamente dentro de la tabla.

Closures

Un closure es una función que ha sido definida dentro de otra función. Así un closure es toda función anidada, ya sea anónima o no.

Los closures se caracterizan por tener acceso a las variables locales que han sido definidas dentro de la función que las contiene.

Veamos un ejemplo de closure.

function externa(a)
    local b = 10
    local c = 5
    return function()
        return a + b + c
    end
end

local f = externa(1)
print(f()) 

Hemos definido una función externa() que acepta un argumento. Dentro de ella se definen dos variables b y c. Esta función devuelve una función anónima que realiza la suma de las variables abc.

Al ejecutar el código del ejemplo se obtiene el siguiente resultado.

16

El concepto más importante a retener dentro de las closures es aquel del acceso a las variables locales de la función envolvente, a que ahí reside su utilidad. Veamos otro ejemplo para entender mejor el funcionamiento de los closures.

function contador()
    local i = 0
    return function()
        i = i + 1
        return i
    end
end

local contador1 = contador()
local contador2 = contador()

contador1()
contador2()
contador2()

print (contador1(), contador2())

En este ejemplo hemos definido de nuevo una función anónima dentro de otra, pero en esta ocasión no acepta ningún argumento. Dentro de la función hemos declarado una variable i que será usada para mantener el estado de la función. La función anónima devuelta incrementa i y devuelve su valor.

Para probar la función hemos declarado dos variables que son inicializadas con sendas referencias a la función contador(). Esto creará dos instancias de la función contador() cada una de ellas con su contador interno.

La primera llamada a contador1() incrementa i en una unidad. Del mismo modo las llamadas a contador2() incrementan el contador en una unidad cada una. Finalmente, durante la llamada a print(), volvemos a llamar tanto a contador1() como a contador2() lo cual incrementa de nuevo los contadores y devuelve su valor final. Así, el resultado que obtenemos es:

2       3

Módulos

En este artículo vamos a presentar qué son los módulos en Lua. Posteriormente veremos como definir diferentes tipos de módulos en las versiones modernas de Lua. Por último y como referencia, hablaremos de la definición de módulos en Lua para versiones más antiguas, en las que los módulos se definían de otra forma.

¿Qué son los módulos en Lua?

Los módulos, en Lua, son la forma que ofrece el lenguaje para encapsular código de forma que pueda ser reutilizado en cualquier programa. Un módulo en Lua es lo que llamamos librería en otros lenguajes. En cualquier caso la idea que reside en un módulo es la de exportar una serie de funcionalidades que puedan ser usadas en otros programas.

Lua no impone ninguna política estricta de cómo deben ser definidos los módulos, con lo que pueden ser definidos según el estilo de cada programador.

Definiendo módulos en Lua

Como comentamos anteriormente, en Lua no hay definida una política estricta de cómo debe ser definido un módulo, dejándolo al estilo del programador. Desde un punto de vista práctico vamos a ver tres casos distintos de definición de un módulo según unos usos específicos. Estos son:

Veamos a continuación estos tres casos de uso en más detalle.

Definir un módulo con una interfaz a través de una tabla

Este caso es el más común y recomendable para aquellos módulos que están orientados a ser distribuidos como una librería. En él, definimos una tabla que contiene una referencia a todas las funciones y variables del módulo que queremos que sean públicas quedando el resto de variables y funciones ocultas fuera del módulo. Esto se consigue declarando las funciones y variables como locales. Una vez definida la tabla, esta es devuelta. Veamos un ejemplo para entender mejor la estructura de un módulo.

Primeramente vamos a definir el módulo en si, para ello creamos un archivo llamado modulo1.lua. El archivo lo podemos llamar como queramos, lógicamente, pero para el tutorial usaremos ese nombre. El nombre del archivo define el nombre del módulo.

-- Definición de modulo1

-- Declaración de la tabla que contendrá las funciones del módulo
M = {}

-- Definición de funciones

-- Calcula el cuadrado de un número
local function cuadrado(x)
   return x * x
end

-- Calcula el cubo de un número
local function cubo(x)
   return x * x * x
end

-- Determina si un número es par
local function esPar(x)
   return x % 2 == 0
end

-- Se asignan las funciones cuadrado, cubo y esPar a la tabla M
-- Esta asignación constituye la interfaz del módulo.
M.cuadrado = cuadrado
M.cubo = cubo
M.esPar = esPar

-- Retorno de la tabla M
return M

En el módulo hemos definido tres funciones matemáticas sencillas de ámbito local. También se ha declarado una tabla, compuesta de tres elementos a los cuales se les han asignado una referencia a cada una de las funciones. Las claves de estos tres elementos coinciden con el nombre de las funciones a las que hacen referencia.

Ahora necesitamos un segundo archivo en el que definiremos el código que se encargará de consumir el módulo. Este archivo lo crearemos en el mismo directorio donde reside el archivo del módulo.

-- Referencia al módulo que queremos cargar
local modulo1 = require 'modulo1'

print("Cuadrado de 5:", modulo1.cuadrado(5))
print("Cubo de 5:", modulo1.cubo(5))
print("5 es par?", modulo1.esPar(5))

Observa que hemos incluido una referencia al módulo modulo1 usando la función require. Como puedes ver, la referencia al módulo se hace por su nombre de archivo descartando la extensión .lua. El resultado de ejecutar el archivo es:

Cuadrado de 5:  25
Cubo de 5:      125
5 es par?       false

Definir un módulo como un objeto

Un módulo que es definido como un objeto permite crear varias instancias del objeto, creando objetos que tienen espacios de memoria diferentes. Esto evita que la manipulación de un objeto afecte a otro. Este paradigma es el mismo que en la programación orientada a objetos. Veamos a continuación un ejemplo para entender mejor este concepto.

Partimos del ejemplo anterior y llamaremos al archivo modulo2.lua. En esta versión vamos a refactorizar la definición del módulo y le añadiremos una nueva función contador.

-- Declaración del módulo
local mod = function()
    -- Declaración de la tabla que contendrá las funciones del módulo
    local M = {}
    local contador = 0
    
    -- Definición de funciones
    
    -- Calcula el cuadrado de un número
    local function cuadrado(x)
        return x * x
    end
    
    -- Calcula el cubo de un número
    local function cubo(x)
        return x * x * x
    end
    
    -- Determina si un número es par
    local function esPar(x)
        return x % 2 == 0
    end

    -- Incrementa un contador
    local function incrementarContador()
        contador = contador + 1
        return contador
    end
    
    -- Se asignan las funciones cuadrado, cubo y esPar a la tabla M
    -- Esta asignación constituye la interfaz del módulo.
    M.cuadrado = cuadrado
    M.cubo = cubo
    M.esPar = esPar
    M.incrementarContador = incrementarContador
    
    -- Retorno de la tabla M
    return M
end

return mod

En este ejemplo, hemos definido el módulo como una función anónima asignada a la variable mod. Dentro de la función anónima declaramos el módulo, al igual que en el archivo modulo1.lua y le añadimos una nueva función. Esta función nos permitirá verificar el aislamiento entre instancias. En el módulo hemos definido una variable contador que está disponible en el ámbito del módulo, por lo que su supervivencia está ligada a la del módulo mismo. Cada llamada a la función incrementarContador() incrementará contador en una unidad.

Desde un punto de vista técnico, en la definición de este módulo, estamos haciendo uso de los closures de Lua, definiendo una serie de funciones dentro de una función envolvente.

El funcionamiento del módulo es sencillo: cada vez que llamamos al módulo, consumimos la función anónima, la cual nos devuelve una referencia distinta de la tabla interna M. Esto nos da la garantía de que las instancias están aisladas entre si. Definamos ahora el código de prueba.

local modulo2 = require 'modulo2'
 
local inst1 = modulo2()
local inst2 = modulo2()
local inst3 = modulo2()

inst1.incrementarContador()

inst2.incrementarContador()
inst2.incrementarContador()

inst3.incrementarContador()
inst3.incrementarContador()
inst3.incrementarContador()

print("Cuadrado de 5:", inst1.cuadrado(5))
print("Cubo de 5:", inst1.cubo(5))
print("5 es par?", inst1.esPar(5))
print("Contador:", inst1.incrementarContador())
print()
print("Cuadrado de 3:", inst2.cuadrado(3))
print("Cubo de 3:", inst2.cubo(3))
print("3 es par?", inst2.esPar(3))
print("Contador:", inst2.incrementarContador())
print()
print("Cuadrado de 2:", inst3.cuadrado(2))
print("Cubo de 2:", inst3.cubo(2))
print("2 es par?", inst3.esPar(2))
print("Contador:", inst3.incrementarContador())

Al igual que en el ejemplo anterior, hacemos referencia al módulo, en este caso modulo2. Creamos tres instancias del módulo, consumiento la función anónima y asignando el valor devuelto a variables distintas. Para probar el aislamiento hacemos varias llamadas a la funcion incrementarContador() en número variable para cada instancia. Finalmente imprimimos el resultado con diversos valores para cada instancia. El resultado que obtenemos es el siguiente:

Cuadrado de 5:  25
Cubo de 5:      125
5 es par?       false
Contador:       2

Cuadrado de 3:  9
Cubo de 3:      27
3 es par?       false
Contador:       3

Cuadrado de 2:  4
Cubo de 2:      8
2 es par?       true
Contador:       4

Observa que para inst1 hemos llamado dos veces incrementarContador(), tres veces para el caso de inst2 y de cuatro veces para inst3. Demostrando el aislamiento de las instancias.

Definición de un módulo como un singleton

En este caso también se define el módulo como un objeto, pero esta vez no se busca el aislamiento, sino más bien todo lo contrario. El singleton es un modelo de programación que busca tener una sola instancia de un objeto disponible para toda la aplicación. Esta técnica se usa, por ejemplo, para almacenar valores o datos que deben ser comunes para toda la aplicación. Un caso típico son los parámetros de la aplicación.

Vamos a ver un ejemplo, en el cual vamos a definir un módulo tipo singleton que contiene los parámetros de conexión a una base de datos. Partiremos de una tabla de parámetros por defecto y, posteriormente los modificaremos.

local instancia
local parametrosPorDefecto = {}

-- Definición de los parámetros por defecto
parametrosPorDefecto["host"] = "localhost"
parametrosPorDefecto["port"] = 5432
parametrosPorDefecto["dbname"] = "postgres"
parametrosPorDefecto["user"] = "postgres"

local mod = function()
    if not instancia then
        -- Declaración de las funciones
        local function obtenerHost ()
            return parametrosPorDefecto["host"]
        end

        local function obtenerPort ()
            return parametrosPorDefecto["port"]
        end

        local function obtenerDbname ()
            return parametrosPorDefecto["dbname"]
        end

        local function obtenerUser ()
            return parametrosPorDefecto["user"]
        end

        local function establecerHost (host)
            parametrosPorDefecto["host"] = host
        end

        local function establecerPort (port)
            parametrosPorDefecto["port"] = port
        end

        local function establecerDbname (dbname)
            parametrosPorDefecto["dbname"] = dbname
        end

        local function establecerUser (user)
            parametrosPorDefecto["user"] = user
        end

        -- Declaración de la tabla que contendrá las funciones del módulo
        local M = {}

        M.obtenerHost = obtenerHost
        M.obtenerPort = obtenerPort
        M.obtenerDbname = obtenerDbname
        M.obtenerUser = obtenerUser
        M.establecerHost = establecerHost
        M.establecerPort = establecerPort
        M.establecerDbname = establecerDbname
        M.establecerUser = establecerUser

        instancia = M
    end

    return instancia
end

return mod

Ahora definamos el código para consumir el módulo.

local modulo3 = require 'modulo3'

local inst1 = modulo3()
local inst2 = modulo3()
local inst3 = modulo3()

inst1.establecerHost("10.123.67.90")
inst1.establecerPort(5433)

inst2.establecerHost("67.98.234.1")
inst2.establecerUser("my_user")

print("Host de inst3:", inst3.obtenerHost())
print("Port de inst3:", inst3.obtenerPort())
print("Dbname de inst3:", inst3.obtenerDbname())
print("User de inst3:", inst3.obtenerUser())

Ahora ejecutamos el código anterior y obtenemos el siguiente resultado:

Host de inst3:  67.98.234.1
Port de inst3:  5433
Dbname de inst3:        postgres
User de inst3:  my_user

Como era de esperar, el valor final de los parámetros es aquel de la última modificación. El valor de host se ha modificado dos veces, su valor final es el de la última modificación (67.98.234.1). Para el valor de dbname tenemos postgres que es el valor por defecto, ya que ninguna llamada a instancia modificó este valor. Observa que hemos modificado el objeto desde tres instancias distintas, pero internamente hemos trabajado con la misma tabla. Con esto se demuestra el funcionamiento de un singleton.

La función require

La función require se usa para cargar y ejecutar módulos o librerías en Lua. Esta función se encarga de buscar el módulo y posteriormente verificar si ya ha sido ejecutado, para evitar asi, ejecutarlo dos veces.

La función require acepta un argumento, que es la ruta del módulo, que es en esencia, el nombre del módulo que queremos cargar.

Recordemos que el nombre de un módulo es el nombre del archivo donde está definido el módulo sin la extensión .lua.

Cuando requerimos el modulo, por ejemplo, modulo3 Lua buscará el módulo en varios lugares distintos. Si no lo encuentra nos mostrará los lugares donde lo buscó:

no field package.preload['module3']
no file 'C:\Utilities\Lua\5.4.2\lua\module3.lua'
no file 'C:\Utilities\Lua\5.4.2\lua\module3\init.lua'
no file 'C:\Utilities\Lua\5.4.2\module3.lua'
no file 'C:\Utilities\Lua\5.4.2\module3\init.lua'
no file 'C:\Utilities\Lua\5.4.2\..\share\lua\5.4\module3.lua'
no file 'C:\Utilities\Lua\5.4.2\..\share\lua\5.4\module3\init.lua'
no file '.\module3.lua'
no file '.\module3\init.lua'
no file 'C:\Utilities\Lua\5.4.2\module3.dll'
no file 'C:\Utilities\Lua\5.4.2\..\lib\lua\5.4\module3.dll'
no file 'C:\Utilities\Lua\5.4.2\loadall.dll'
no file '.\module3.dll'
no file 'C:\Utilities\Lua\5.4.2\module354.dll'
no file '.\module354.dll'

Esos lugares se corresponden con unos patrones definidos en Lua para buscar los módulos. En este ejemplo de salida, C:\Utilities\Lua\5.4.2 es donde se encuentra el ejecutable de Lua, que se corresponde con la variable PATH.

En todo caso, podemos definir la ruta completa al modulo, pero no funcionaría correctamente al portar el código a otras plataformas. Una solución, sería definir los módulos en una carpeta llamada ´modulos´ y requerir los módulos incluyendo la carpeta:

require "modulos/modulo3"

Definición de módulos en versiones antiguas

En las versiones 5.0 y 5.1 de Lua, los módulos se definían de un modo diferente. A modo de referencia, veamos como era:

-- Definición de modulo1
module("modulo1", package.seeall)

-- Definición de funciones

-- Calcula el cuadrado de un número
function cuadrado(x)
    return x * x
end

-- Calcula el cubo de un número
function cubo(x)
    return x * x * x
end

-- Determina si un número es par
function esPar(x)
    return x % 2 == 0
end

En estas versiones, los modulos se usaban del siguiente modo:

require("modulo1")
modulo1.cuadrado(5)
modulo1.cubo(3)
modulo1.esPar(10)

Las versiones modernas no soportan este modelo, por lo que si ejecutas el código obtendrás un error.