Jump to content

Module:module loader

From Wiktionary, the free dictionary

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:

  • 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

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 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.


--[==[ 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