require paso a paso
El algoritmo completo de require: la consulta al caché de package.loaded, el recorrido ordenado de la lista de buscadores, el registro directo de package.preload, la acumulación de mensajes de fallo y el momento exacto en que el módulo entra en el caché.
require cabe en una pantalla de código C y no hace nada que no pudieras escribir tú en Lua. Su algoritmo tiene cuatro fases: mirar si ya está, buscar quién sabe cargarlo, ejecutar a ese cargador y anotar el resultado. Cada una de las cuatro esconde una decisión de diseño con consecuencias observables —el caché explica por qué un módulo es un singleton, la lista ordenada de buscadores explica las precedencias, y el instante preciso en que se escribe el caché explica qué ocurre con las dependencias circulares—. Esta lección recorre el algoritmo literal, sin metáforas.
- Reproducir el algoritmo de
requireen sus cuatro fases y en su orden exacto. - Explicar el papel de
package.loadedcomo caché y como registro de módulos precargados. - Registrar cargadores a mano mediante
package.preloady añadir buscadores propios. - Interpretar el mensaje de error acumulado y razonar sobre las dependencias circulares.
El caché manda
Lo primero que hace require con el nombre recibido es consultar package.loaded. Si ese campo contiene un valor verdadero, lo devuelve de inmediato y termina: no toca el disco, no consulta rutas y no ejecuta nada.
print(package.loaded.string == string) --> true
print(package.loaded.math == math) --> true
Las bibliotecas estándar ya están ahí desde el arranque, y esa es la razón de que require("string") sea instantáneo y devuelva la misma tabla que la variable global. package.loaded no es un caché de ficheros: es el registro de todos los módulos vivos del estado de Lua, y el nombre del módulo es su clave.
El detalle de que la comprobación sea de veracidad y no de existencia importa. Si escribes package.loaded.mimodulo = false, el módulo se considerará no cargado y se buscará otra vez, mientras que el campo sigue estando presente. Es una asimetría deliberada que casi nunca se usa a propósito y que a veces se sufre por accidente.
La lista de buscadores
Si el caché no responde, require recorre package.searchers en orden, desde el índice 1 hacia arriba. Cada elemento es una función que recibe el nombre del módulo y devuelve una de dos cosas: un cargador —una función que sabe producir el módulo— junto con un dato extra, o bien una cadena que explica por qué ese buscador no ha podido.
for i, s in ipairs(package.searchers) do
print(i, s)
end
--> 1 function: 0x... preload
--> 2 function: 0x... ficheros de Lua
--> 3 function: 0x... bibliotecas C
--> 4 function: 0x... todo en uno
El primero consulta package.preload. El segundo busca un fichero de Lua usando package.path. El tercero busca una biblioteca dinámica usando package.cpath. El cuarto, el llamado todo en uno, sirve al caso en que varios submódulos viven dentro de una misma biblioteca C: para el nombre a.b busca la biblioteca de la raíz a y dentro de ella la función de apertura correspondiente al submódulo.
Que sea una lista y no un procedimiento fijo es lo que hace extensible al sistema. Insertar un buscador propio es insertar un elemento en una tabla.
table.insert(package.searchers, 2, function(nombre)
local fuente = mi_repositorio[nombre]
if not fuente then
return "\n\tno esta en el repositorio en memoria"
end
return assert(load(fuente, "@" .. nombre)), ":memoria:"
end)
La cadena devuelta en caso de fallo empieza por un salto de línea y una tabulación porque require la concatena con las de los demás buscadores para construir el mensaje de error final. Ese formato no es capricho: es el contrato implícito de la interfaz.
El nombre package.searchers es de 5.2 en adelante. LuaJIT, anclado en la semántica de 5.1, sigue usando package.loaders. El código que quiera funcionar en ambos debe elegir la tabla con package.searchers or package.loaders, y no puede dar por supuesto el mismo número de buscadores ni el mismo orden.
preload: un módulo sin fichero
package.preload es una tabla que asocia nombres de módulo a funciones cargadoras. El primer buscador se limita a mirar ahí, y si encuentra algo lo devuelve como cargador. No hay disco, no hay rutas, no hay extensión de fichero.
package.preload["config"] = function(nombre)
return { entorno = "produccion", pedido_como = nombre }
end
local cfg = require("config")
print(cfg.entorno) --> produccion
print(cfg.pedido_como) --> config
Este mecanismo es la base de tres cosas muy distintas. Es como los binarios estáticos empotran módulos de Lua dentro del ejecutable sin ficheros sueltos. Es como se sustituye un módulo por un doble durante las pruebas, registrando el doble antes de que nadie pida el original. Y es como se registran desde C los módulos escritos en C que se enlazan estáticamente.
Fíjate en que el cargador recibe el nombre como primer argumento, igual que lo recibiría un chunk cargado de un fichero. La interfaz del cargador es siempre la misma, venga de donde venga.
Hay una asimetría entre package.preload y package.loaded que conviene fijar porque las dos tablas se confunden con frecuencia. La primera guarda cómo fabricar un módulo; la segunda guarda el módulo ya fabricado. Escribir en preload no carga nada, solo deja preparada la receta para cuando alguien la pida; escribir en loaded salta la fabricación entera y entrega directamente el valor.
package.preload["tardio"] = function() print("me ejecuto ahora") ; return {} end
print(package.loaded["tardio"]) --> nil, todavia no se ha pedido
local t = require("tardio") --> imprime: me ejecuto ahora
print(package.loaded["tardio"] == t) --> true, ya esta fabricado
De ahí se sigue la regla de los dobles de prueba: registrar en preload sirve si el módulo aún no se ha pedido; si ya está en loaded, la receta nunca llegará a ejecutarse.
Qué se guarda y qué se devuelve
Localizado el cargador, require lo llama con dos argumentos: el nombre del módulo y el dato extra que devolvió el buscador. Después aplica dos reglas encadenadas que conviene memorizar en este orden.
Primera: si el cargador devuelve un valor distinto de nil, ese valor se escribe en package.loaded bajo el nombre pedido. Segunda: si tras eso package.loaded sigue sin valor para ese nombre —porque el cargador devolvió nil y tampoco escribió el campo por su cuenta—, se guarda true. Finalmente require devuelve lo que haya en el caché y, desde 5.4, también el dato del buscador.
flowchart TB A[require con un nombre] --> B[consulta package loaded] B -->|valor verdadero| C[devolver de inmediato] B -->|nada| D[buscador 1 mira package preload] D -->|falla| E[buscador 2 usa package path] E -->|falla| F[buscador 3 usa package cpath] F -->|falla| G[buscador 4 todo en uno] G -->|falla| H[error con los mensajes concatenados] D -->|acierta| L[llamar al cargador con nombre y dato extra] E -->|acierta| L F -->|acierta| L G -->|acierta| L L --> M[guardar el retorno en package loaded] M --> N[devolver el valor guardado]
El orden temporal de esas escrituras tiene una consecuencia mayor: el módulo no está en el caché mientras se está ejecutando. Si a pide b en su cabecera y b pide a, cuando b llame a require("a") el caché seguirá vacío para a, porque a no ha llegado a su return. El resultado es una recursión.
Lua 5.2 marcaba el módulo en curso con un valor centinela y abortaba con loop or previous error loading module. Las versiones 5.3 y 5.4 retiraron ese centinela, de modo que hoy un ciclo se manifiesta como recursión indefinida hasta desbordar la pila de C. El diagnóstico es peor y la lección es la misma: los ciclos entre módulos no se arreglan, se eliminan, extrayendo lo compartido a un tercer módulo o retrasando el require al punto de uso.
Cuando todos los buscadores fallan, el error reúne sus explicaciones y es sorprendentemente informativo si uno se molesta en leerlo entero.
lua -e 'require("no_existe")'
# module 'no_existe' not found:
# no field package.preload['no_existe']
# no file './no_existe.lua'
# no file '/usr/local/share/lua/5.4/no_existe.lua'
# no file './no_existe.so'
Cada línea es un intento concreto con una ruta concreta. No dice que el módulo no exista: dice exactamente dónde miró.
Es tentador leer package.loaded como una memoria de rendimiento que evita releer ficheros. Esa lectura es correcta y es la menos importante. Lo que el caché establece de verdad es una identidad: garantiza que todo el programa que pida un nombre reciba el mismo valor, y por tanto convierte cada módulo en un singleton de proceso cuyo estado interno es compartido por todos sus consumidores sin que ninguno lo sepa. De ahí se derivan casi todos los comportamientos difíciles del sistema. Se deriva que un módulo con estado mutable sea, de facto, una variable global con mejores modales. Se deriva que las metatablas definidas dentro de un módulo sirvan como marcas de tipo fiables, porque la comparación de identidad contra la tabla del módulo funciona en todo el proceso. Se deriva que vaciar una entrada del caché no recargue nada, sino que cree un segundo universo que convive con el primero, con dos tablas distintas que dicen llamarse igual y con objetos antiguos apuntando a la versión vieja. Y se deriva, sobre todo, que la clave del caché sea la cadena con la que se pidió el módulo, no el fichero resuelto: dos nombres distintos que acaben en el mismo fichero producen dos módulos independientes, con dos copias del estado, y ese es el fallo más difícil de diagnosticar de todo el sistema, porque el código fuente de ambos es idéntico byte a byte. La regla práctica que se sigue de todo esto es breve y vale para cualquier base de código seria: un nombre canónico por módulo, escrito siempre igual, y estado mutable en los módulos solo cuando quieras de verdad que sea global.
- Imprime las claves de
package.loadedal arrancar el intérprete y explica por qué están todas ahí antes de que tu programa pida nada. - Escribe una función
mi_requireen Lua puro que reproduzca las cuatro fases usandopackage.loadedypackage.searchers, y compara su salida con la derequiresobre varios módulos. - Registra un doble de prueba en
package.preloadantes de cargar un módulo real y verifica que el original nunca se toca. - Inserta un buscador propio en la posición 2 que resuelva nombres desde una tabla en memoria. Comprueba qué cambia si lo insertas al final de la lista.
- Provoca una dependencia circular entre dos módulos, observa el fallo concreto de tu versión de Lua y reescribe el par para eliminar el ciclo.