Qué es un módulo en Lua
Lua no tiene ninguna palabra clave para declarar módulos. Un módulo es un chunk compilado como función vararg cuyo valor de retorno alguien guarda en un caché; la tabla, el nombre y el espacio de nombres son convención sostenida por una única función de biblioteca.
En casi todos los lenguajes, importar es sintaxis: hay una palabra reservada, un compilador que la entiende y un grafo de dependencias que alguien puede dibujar sin ejecutar nada. En Lua no existe nada de eso. No hay import, no hay module, y el intérprete no sabe qué es un módulo: ni el analizador léxico ni el generador de código contienen la palabra. Lo que hay es una función de biblioteca llamada require, una tabla llamada package y un acuerdo entre programadores. Comprender que el sistema de módulos vive entero en la biblioteca estándar, y no en el lenguaje, es lo que hace comprensible todo lo demás de este nivel.
- Explicar por qué Lua carece de construcción sintáctica para declarar un módulo.
- Describir un chunk como función vararg y leer los argumentos que
requirele entrega. - Reconocer el idioma de la tabla local devuelta al final y sus variantes legítimas.
- Separar lo que
requireimpone de lo que solo es convención de la comunidad.
No existe la palabra módulo
Busca module en la gramática de Lua 5.4 y no la encontrarás. Hubo una época en que sí existía algo parecido: Lua 5.0 y 5.1 traían una función de biblioteca llamada module, que se invocaba en la primera línea del fichero y reescribía el entorno del chunk para que toda asignación posterior cayese dentro de una tabla con el nombre del módulo.
-- Estilo de Lua 5.1, hoy retirado
module("geometria", package.seeall)
function area_circulo(r) -- acaba en geometria.area_circulo
return math.pi * r * r
end
Aquello se desaconsejó en 5.2 y desapareció del núcleo poco después. Las razones son instructivas porque son las mismas que hacen elegante lo que vino a sustituirlo. module mutaba el entorno global de forma no local y sin aviso; el famoso package.seeall abría un agujero por el que todo el estado global entraba en el módulo y salía de él; y peor aún, registraba el módulo en la tabla global bajo su propio nombre, de modo que cargar un fichero tenía como efecto secundario crear una variable global.
Lo que quedó tras la retirada no fue un sustituto, sino la ausencia de sustituto. Un módulo en Lua moderno es un fichero corriente que no hace ninguna declaración especial y que termina con un return. Todo el mecanismo se concentra en require, y require es una función normal que podrías escribir tú.
Un fichero es una función vararg
Cuando Lua compila un fichero produce un chunk, y un chunk es exactamente una función con parámetros variables cuyo _ENV es su primer upvalue. No es una analogía: es la representación literal. De ahí se deriva todo lo que sigue.
Como es una función, puede devolver valores. Como es vararg, puede recibir argumentos, y require le pasa dos: el nombre bajo el que se pidió el módulo y el dato que produjo el buscador que lo localizó, que para un fichero de Lua es la ruta resuelta.
-- app/util.lua
local nombre, origen = ...
print(nombre) --> app.util
print(origen) --> ./app/util.lua
return {}
Conviene subrayar un matiz que sorprende a mucha gente: nombre no se deriva del fichero, sino de la cadena que escribió quien llamó a require. Si el mismo fichero se alcanza desde dos nombres distintos, recibirá dos nombres distintos y se cargará dos veces. El fichero no sabe cómo se llama; sabe cómo lo llamaron.
Como es una función, también tiene sus propios locales. Todo lo que declares con local en el cuerpo del fichero es invisible desde fuera: no hay nada que exportar salvo lo que decidas devolver. Esa es la privacidad real de un módulo en Lua, y no depende de ninguna palabra reservada, sino del ámbito léxico que ya conoces del nivel 9.
La convención de la tabla
El idioma canónico ocupa tres gestos: una tabla local al principio, funciones colgadas de ella y un return al final.
-- geometria.lua
local M = {}
function M.area_circulo(r)
return math.pi * r * r
end
function M.area_rectangulo(base, altura)
return base * altura
end
return M
local geo = require("geometria")
print(geo.area_circulo(2)) --> 12.566370614359
La tabla no es especial en ningún sentido: es una tabla. Se llama M por costumbre, y ese nombre no aparece en ninguna documentación normativa. El return M de la última línea es lo único que conecta el fichero con quien lo pide, y si lo olvidas el módulo no falla al cargar, simplemente entrega un valor inútil.
De ese carácter ordinario se sigue algo que en otros lenguajes exigiría maquinaria dedicada: el valor de un módulo se puede pasar por parámetro, guardar en otra tabla, comparar por identidad, recorrer con pairs o modificar desde fuera. Un espacio de nombres en Lua es un dato como cualquier otro, y la lección 13.4 examinará las consecuencias de que también sea modificable.
El chunk
Una función vararg con su propio _ENV. Se ejecuta una sola vez, y lo que declares con local en él queda fuera del alcance de cualquiera.
El nombre
Una cadena elegida por quien llama, no derivada del fichero. Es la clave del caché y la única identidad que el módulo tiene en el proceso.
El valor devuelto
Lo que el return final entregue. Suele ser una tabla porque es cómodo, pero require acepta cualquier valor y sustituye la ausencia por true.
require devuelve el valor del módulo y, como segundo resultado, el dato del buscador que lo encontró. Escribir local m, ruta = require("app.util") te da la ruta exacta desde la que se cargó, lo que resuelve de un plumazo la mitad de los problemas de diagnóstico de la lección 13.3. En 5.3 y anteriores solo se devuelve el primer valor.
Lo que la convención no obliga
Nada en require exige una tabla. El chunk puede devolver cualquier valor, y hay módulos legítimos que devuelven una sola función.
-- trim.lua
return function(s)
return (s:gsub("^%s*(.-)%s*$", "%1"))
end
Si el chunk no devuelve nada, require no falla: guarda el booleano true en el caché para dejar constancia de que ese nombre ya se cargó. Es lo que ocurre con los módulos que solo tienen efectos secundarios, y explica el desconcierto de ver true donde esperabas una tabla.
La otra mitad de la convención la aporta el caché, que es el tema de la lección siguiente pero que ya conviene ver en acción, porque es lo que distingue cargar de importar.
local a = dofile("geometria.lua")
local b = dofile("geometria.lua")
print(a == b) --> false: dos tablas distintas
local c = require("geometria")
local d = require("geometria")
print(c == d) --> true: el mismo valor
dofile ejecuta; require importa. La diferencia entera está en que el segundo recuerda lo que ya hizo, y de esa memoria se sigue que el valor de un módulo es un singleton para todo el proceso.
flowchart LR F[fichero de texto] --> C[chunk compilado como funcion vararg] C -->|recibe nombre y origen| E[se ejecuta una sola vez] E --> R[valor de retorno cualquiera] R --> K[se guarda en el cache bajo el nombre pedido] K --> U[todos los que lo piden reciben ese mismo valor]
La decisión de no dar sintaxis a los módulos no es una omisión ni un signo de inmadurez: es la doctrina del nivel 1 aplicada con rigor. En Python o en JavaScript, import es una declaración que el compilador entiende; existe un grafo de dependencias estático, y sobre él se pueden construir análisis, empaquetado, eliminación de código muerto y detección de ciclos. Lua renuncia a todo eso a cambio de una propiedad distinta: el sistema de módulos es programable en tiempo de ejecución por el propio programa. Puedes añadir un buscador que resuelva nombres contra una base de datos, contra un archivo comprimido en memoria o contra la red; puedes envolver require para instrumentar cada carga; puedes darle a un subsistema su propio caché y su propio conjunto de rutas sin tocar el resto; puedes reemplazar la función entera. Nada de esto es un truco: es la consecuencia de que package sea una tabla ordinaria y require una función ordinaria, ambas alcanzables y ambas sustituibles. El precio se paga en el otro lado y hay que nombrarlo sin adornos: no hay forma fiable de saber qué carga un programa sin ejecutarlo, no hay verificación de que un módulo exporte lo que promete, no hay frontera que el lenguaje defienda, y el nombre de un módulo es una cadena que se construye en tiempo de ejecución con las reglas que a cada cual le parezcan. Quien viene de un lenguaje con módulos de primera clase tiende a leer esto como carencia y a reconstruir la garantía perdida con capas de comprobaciones. El instinto correcto es el contrario: aceptar que en Lua la disciplina modular no la impone el compilador sino el equipo, y que a cambio dispones de un mecanismo de carga que puedes doblar por completo hacia donde tu programa lo necesite.
- Escribe
geometria.luacon el idioma canónico y compruébalo conrequire. Después borra elreturn Mfinal y observa qué valor recibe el llamador. - Añade al fichero una primera línea que capture
...y imprima ambos argumentos. Pide el módulo con dos grafías distintas del mismo nombre y explica por qué se ejecuta dos veces. - Escribe un módulo que devuelva una sola función y otro que no devuelva nada. Inspecciona en ambos casos qué guardó
require. - Compara
dofileyrequiresobre el mismo fichero con un contador que se incremente al cargar, y deduce del resultado dónde vive la memoria. - Declara una función sin
localdentro de un módulo, cárgalo y comprueba desde fuera que la variable existe en el entorno global. Argumenta por qué eso es un fallo aunque el programa funcione.