Module:module loader
- The following documentation is located at Module:module loader/documentation. [edit] Categories were auto-generated by Module:documentation. [edit]
- Useful links: subpage list • links • transclusions • testcases • sandbox
This module lets a module declare all of its dependencies at the top, in one place, while still loading each of them only when the code reaches it. A page which never enters a given branch never pays for the modules that branch needs.
Call init once and keep the table it returns:
local M = require("Module:module loader").init{
require = {
utilities = "Module:utilities",
languages = "Module:languages",
},
loadData = {
headword_data = "Module:headword/data",
},
lazy = {
lang = function()
return require("Module:languages").getByCode("de")
end,
},
}
M.utilities.format_categories(...) -- requires Module:utilities
M.headword_data.page -- mw.loadData on first access
M.lang:makeEntryName(...) -- runs the getter on first access
The table init returns is a proxy. Reading a name for the first time does the work, and the result is rawset onto the proxy, so every later read is a plain table lookup and no metamethod runs. The same applies one level down: for a require entry, reading M.name.member caches that member on the sub-proxy.
What a name reads as depends on the field it was declared in:
requireandrequireOptionalgive a sub-proxy, not the module table. Key lookups pass through, so the module's own__indexstill fires, but only keys already read exist on the proxy itself, sopairsand#see only those. Metamethods like__callapply to the module, not to the proxy that stands in for it.loadDataandloadDataOptionalgive themw.loadDatatable itself, which behaves like any other table, so iteration works.lazygives whatever the getter returned.
A name which was never declared reads as nil, as does an optional module which failed to load. The failure is cached, so a missing module is looked up once and not on every access. Guard with if M.foo then instead of indexing straight into it. A module declared under require which returns a function rather than a table is cached and returned as that function, with no proxy around it.
The laziness is per-access, not per-call. The proxy defers until a name is read, not until the value that name holds is called, so binding a name to a local at the top of a module loads it there and then:
local shallowCopy = M.table.shallowCopy -- loads Module:table immediately
This applies to every field, lazy included, since local x = M.x is itself the access which runs the getter. Keep the access at the call site instead, where it costs two table lookups once the first access has cached both levels. Where a bare local is wanted, use the self-overwriting loader idiom of Module:table and Module:parameters, which defers to the first call and costs nothing after it, because the local is replaced by the target function:
local function shallowCopy(...)
shallowCopy = require("Module:table").shallowCopy
return shallowCopy(...)
end
export.init
function export.init(opts)
Given a table of dependency declarations opts, returns the lazy proxy described above. Call it once, at the top of a module.
opts may contain any of the following fields, all optional:
require: maps a short name to a module path, which is loaded withrequireon first access.loadData: maps a short name to a data module path, which is loaded withmw.loadDataon first access. The table is returned as-is.requireOptional: asrequire, but the load runs underpcall, and a module which is missing or throws an error reads asnil. Such a module must return a table; one returning a function counts as missing.loadDataOptional: asloadData, but the load runs underpcall, and a module which is missing or throws an error reads asnil.lazy: maps a short name to a getter for a value which is not a whole module. WhererequireandloadDatadefer loading a module,lazydefers computing a value, such as an object built from a module, or something borrowed from elsewhere which you would rather not touch until it is needed. Each entry is either a function returning the value, or a table of names to such functions, giving a nested object whose keys are each evaluated on their own first access. For instance:lang = function() return require("Module:languages").getByCode("de") end, so that Module:languages is not loaded on pages which never reach the relevant branch.ustring = function() return mw.ustring end, a sub-namespace ofmw.concat = function() return table.concat end, a plain alias.mw = {getCurrentTitle = ..., getCurrentFrame = ..., getContentLanguage = ...}, the nested form, with one deferred key per expensive call.
Throws an error if a short name is declared in more than one field, or if the same module path is declared twice across require and loadData. Paths in the optional fields are not tracked, so a module may be declared both as required and as optional.
--[==[ intro:
This module lets a module declare all of its dependencies at the top, in one place, while still loading each of them
only when the code reaches it. A page which never enters a given branch never pays for the modules that branch needs.
Call `init` once and keep the table it returns:
```
local M = require("Module:module loader").init{
require = {
utilities = "Module:utilities",
languages = "Module:languages",
},
loadData = {
headword_data = "Module:headword/data",
},
lazy = {
lang = function()
return require("Module:languages").getByCode("de")
end,
},
}
M.utilities.format_categories(...) -- requires Module:utilities
M.headword_data.page -- mw.loadData on first access
M.lang:makeEntryName(...) -- runs the getter on first access
```
The table `init` returns is a proxy. Reading a name for the first time does the work, and the result is {rawset} onto
the proxy, so every later read is a plain table lookup and no metamethod runs. The same applies one level down: for a
`require` entry, reading `M.<var>name</var>.<var>member</var>` caches that member on the sub-proxy.
What a name reads as depends on the field it was declared in:
* `require` and `requireOptional` give a sub-proxy, not the module table. Key lookups pass through, so the module's own
{__index} still fires, but only keys already read exist on the proxy itself, so {pairs} and {#} see only those.
Metamethods like {__call} apply to the module, not to the proxy that stands in for it.
* `loadData` and `loadDataOptional` give the {mw.loadData} table itself, which behaves like any other table, so
iteration works.
* `lazy` gives whatever the getter returned.
A name which was never declared reads as {nil}, as does an optional module which failed to load. The failure is cached,
so a missing module is looked up once and not on every access. Guard with {if M.foo then} instead of indexing straight
into it. A module declared under `require` which returns a function rather than a table is cached and returned as that
function, with no proxy around it.
The laziness is per-access, not per-call. The proxy defers until a name is read, not until the value that name holds is
called, so binding a name to a local at the top of a module loads it there and then:
```
local shallowCopy = M.table.shallowCopy -- loads Module:table immediately
```
This applies to every field, `lazy` included, since {local x = M.x} is itself the access which runs the getter. Keep the
access at the call site instead, where it costs two table lookups once the first access has cached both levels. Where a
bare local is wanted, use the self-overwriting loader idiom of [[Module:table]] and [[Module:parameters]], which defers
to the first call and costs nothing after it, because the local is replaced by the target function:
```
local function shallowCopy(...)
shallowCopy = require("Module:table").shallowCopy
return shallowCopy(...)
end
```
]==]
local export = {}
local NIL = {} -- sentinel for optional loadData/require that failed (so we don't retry)
-- Validate the declaration: a short name may appear in only one of the five fields, and a
-- module path may not appear twice across require and loadData.
local function validate(modules, data_modules, optional_data_modules, optional_require_modules, lazy_modules)
local seen_names = {}
local seen_paths = {}
-- Check require modules
for name, path in pairs(modules) do
if seen_names[name] then
error("Module loader: duplicate name '" .. name .. "'")
end
seen_names[name] = "require"
if seen_paths[path] then
error("Module loader: module '" .. path .. "' loaded twice (as '" .. seen_paths[path] .. "' and '" .. name .. "')")
end
seen_paths[path] = name
end
-- Check loadData modules
for name, path in pairs(data_modules) do
if seen_names[name] then
error("Module loader: duplicate name '" .. name .. "' (in both require and loadData)")
end
seen_names[name] = "loadData"
if seen_paths[path] then
error("Module loader: module '" .. path .. "' loaded twice (as '" .. seen_paths[path] .. "' and '" .. name .. "')")
end
seen_paths[path] = name
end
-- Check loadDataOptional (names only; its paths are not tracked)
for name, path in pairs(optional_data_modules or {}) do
if seen_names[name] then
error("Module loader: duplicate name '" .. name .. "'")
end
seen_names[name] = "loadDataOptional"
end
-- Check requireOptional (names only)
for name, path in pairs(optional_require_modules or {}) do
if seen_names[name] then
error("Module loader: duplicate name '" .. name .. "'")
end
seen_names[name] = "requireOptional"
end
-- Check lazy (names only)
for name in pairs(lazy_modules or {}) do
if seen_names[name] then
error("Module loader: duplicate name '" .. name .. "'")
end
seen_names[name] = "lazy"
end
end
-- Build a proxy for a lazy object: table of name → function () → value. Each key is evaluated once and cached.
local function make_lazy_object_proxy(getters)
return setmetatable({}, {
__index = function(proxy, key)
local getter = getters[key]
if type(getter) ~= "function" then return nil end
local value = getter()
rawset(proxy, key, value)
return value
end
})
end
--[==[
Given a table of dependency declarations ``opts``, returns the lazy proxy described above. Call it once, at the top of a
module.
``opts`` may contain any of the following fields, all optional:
* `require`: maps a short name to a module path, which is loaded with {require} on first access.
* `loadData`: maps a short name to a data module path, which is loaded with {mw.loadData} on first access. The table is
returned as-is.
* `requireOptional`: as `require`, but the load runs under {pcall}, and a module which is missing or throws an error
reads as {nil}. Such a module must return a table; one returning a function counts as missing.
* `loadDataOptional`: as `loadData`, but the load runs under {pcall}, and a module which is missing or throws an error
reads as {nil}.
* `lazy`: maps a short name to a getter for a value which is not a whole module. Where `require` and `loadData` defer
loading a module, `lazy` defers computing a value, such as an object built from a module, or something borrowed from
elsewhere which you would rather not touch until it is needed. Each entry is either a function returning the value, or
a table of names to such functions, giving a nested object whose keys are each evaluated on their own first access.
For instance:
** {lang = function() return require("Module:languages").getByCode("de") end}, so that [[Module:languages]] is not
loaded on pages which never reach the relevant branch.
** {ustring = function() return mw.ustring end}, a sub-namespace of {mw}.
** {concat = function() return table.concat end}, a plain alias.
** {mw = {getCurrentTitle = ..., getCurrentFrame = ..., getContentLanguage = ...}}, the nested form, with one deferred
key per expensive call.
Throws an error if a short name is declared in more than one field, or if the same module path is declared twice across
`require` and `loadData`. Paths in the optional fields are not tracked, so a module may be declared both as required and
as optional.]==]
function export.init(opts)
local modules = opts.require or {}
local data_modules = opts.loadData or {}
local optional_data_modules = opts.loadDataOptional or {}
local optional_require_modules = opts.requireOptional or {}
local lazy_modules = opts.lazy or {}
validate(modules, data_modules, optional_data_modules, optional_require_modules, lazy_modules)
local loaded_modules = {}
local function get_module(module_name)
local cached = loaded_modules[module_name]
if cached ~= nil then
if cached == NIL then return nil end
return cached
end
if data_modules[module_name] then
loaded_modules[module_name] = mw.loadData(data_modules[module_name])
elseif optional_data_modules[module_name] then
local ok, data = pcall(mw.loadData, optional_data_modules[module_name])
loaded_modules[module_name] = (ok and data) and data or NIL
elseif optional_require_modules[module_name] then
local ok, mod = pcall(require, optional_require_modules[module_name])
loaded_modules[module_name] = (ok and mod and type(mod) == "table") and mod or NIL
elseif modules[module_name] then
loaded_modules[module_name] = require(modules[module_name])
end
local m = loaded_modules[module_name]
if m == NIL then return nil end
return m
end
local proxy_metatable = {
__index = function(proxy, module_name)
-- Lazy: single getter function or table of getters (nested lazy object)
if lazy_modules[module_name] then
local lazy_def = lazy_modules[module_name]
if type(lazy_def) == "function" then
local value = lazy_def()
rawset(proxy, module_name, value)
return value
end
if type(lazy_def) == "table" then
local sub = make_lazy_object_proxy(lazy_def)
rawset(proxy, module_name, sub)
return sub
end
end
local module = get_module(module_name)
if not module then return nil end
-- If the module itself is a function, cache and return it directly
if type(module) == "function" then
rawset(proxy, module_name, module)
return module
end
-- For loadData / loadDataOptional modules, return the table directly (no function proxying)
if data_modules[module_name] or optional_data_modules[module_name] then
rawset(proxy, module_name, module)
return module
end
-- Everything else (require, and requireOptional that loaded) gets a proxy that
-- caches individual members as they are read.
local function_proxy = setmetatable({}, {
__index = function(function_cache, function_name)
local cached_function = module[function_name]
rawset(function_cache, function_name, cached_function)
return cached_function
end
})
rawset(proxy, module_name, function_proxy)
return function_proxy
end
}
return setmetatable({}, proxy_metatable)
end
return export