Module:fun: Difference between revisions

3,147 bytes added ,  24 April 2025
m
(bot) slight optimization to 5.2 compat: prefer unpack to table.unpack
No edit summary
m ((bot) slight optimization to 5.2 compat: prefer unpack to table.unpack)
Line 1: Line 1:
local export = {}
local export = {}


local getmetatable = getmetatable
local debug_track_module = "Module:debug/track"
local table_get_unprotected_metatable = "Module:table/getUnprotectedMetatable"
 
local chain -- defined below
local chain_iter -- defined below
local format = string.format
local gmatch = string.gmatch
local gmatch = string.gmatch
local ipairs = ipairs
local ipairs = ipairs
local is_callable -- defined below
local pairs = pairs
local pairs = pairs
local pcall = pcall
local pcall = pcall
local rawequal = rawequal
local rawget = rawget
local rawget = rawget
local require = require
local select = select
local select = select
local setmetatable = setmetatable
local tostring = tostring
local tostring = tostring
local type = type
local type = type
local unpack = unpack
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
local unroll -- defined below
local xpcall = xpcall


--[==[
local function debug_track(...)
Loaders for functions in other modules, which overwrite themselves with the target function when called. This ensures modules are only loaded when needed, retains the speed/convenience of locally-declared pre-loaded functions, and has no overhead after the first call, since the target functions are called directly in any subsequent calls.]=
debug_track = require(debug_track_module)
return debug_track(...)
end
 
local function get_unprotected_metatable(...)
get_unprotected_metatable = require(table_get_unprotected_metatable)
return get_unprotected_metatable(...)
end


local function _iterString(iter, i)
local function _iterString(iter, i)
Line 31: Line 45:


--[==[
--[==[
Return {true} if the input is a function or functor (a table which can be called like a function, because it has a {__call} metamethod).
Return {true} if the input is a function or functor (an object which can be called like a function, because it has a {__call} metamethod).


Note: if the input is a table with a protected metatable (i.e. one hidden using the `__metatable` metamethod), then this function will treat the value of `__metatable` as though it is the metatable, as that is what gets returned by `getmetatable` in such cases. If you are making use of the `__metatable` metamethod, make sure that `__metatable` is a table with a function at the `__call` key to ensure that this function returns the correct result; it does not matter if this function is the true `__call` metamethod.]==]
Note: if the input is an object with a {__call} metamethod, but this function is not able to find it because the object's metatable is protected with {__metatable}, then it will return {false} by default, or {nil} if the {allow_maybe} flag is set.]==]
function export.is_callable(f)
function export.is_callable(f, allow_maybe)
local f_type = type(f)
if type(f) == "function" then
if f_type == "function" then
return true
return true
elseif f_type ~= "table" then
return false
end
end
-- A table is a functor if it has a `__call` metamethod. The only way to truly confirm this is by trying to call the table, but that could modify the table or other variables out of scope, so look for a `__call` metamethod instead. If the metatable is protected with `__metatable`, this may not be possible.
-- An object is a functor if it has a `__call` metamethod. The only way to truly confirm this is by trying to call it, but that could be expensive or have side effects, so look for a `__call` metamethod instead. If the metatable is protected with `__metatable`, this may not be possible.
local mt = getmetatable(f)
local mt = get_unprotected_metatable(f)
if mt == nil then
if mt == nil then
return false
return false
-- `get_unprotected_metatable` returns false if the metatable is protected.
elseif mt == false then
debug_track("fun/is_callable/protected metatable")
if allow_maybe then
return nil
end
return false
end
-- `__call` metamethods have to be functions, so don't recurse to check it.
local __call = rawget(mt, "__call")
return __call and type(__call) == "function" or false
end
is_callable = export.is_callable
--[==[
A version of {xpcall} which takes any arguments to be given to {f} as additional arguments after the error handler.
This fixes a deficiency in the standard version of {xpcall}, which is not able to handle arguments to be given to {f}, and brings it in line with {pcall}.]==]
function export.xpcall(f, err_handler, ...)
-- If there are no arguments, just call xpcall() with `f`.
if select("#", ...) == 0 then
return xpcall(f, err_handler)
end
-- Any arguments have to be smuggled in via a table, as ... can't be an
-- upvalue, and it's not possible to use pcall() to get aroud this, because
-- xpcall() calls the error handler before the stack unwinds.
local args = {...}
return xpcall(function()
return f(unpack(args))
end, err_handler)
end
do
local function catch_values(f, success, ...)
if success then
return success, ...
-- Error message will only take this exact form if `f` is not callable,
-- because it will contain a traceback if it was thrown further up the
-- stack.
elseif (...) == format("attempt to call a %s value", type(f)) then
return false
end
return error(...)
end
end
-- Check if the metatable is protected: `setmetatable` will throw an error if so.
 
local success = pcall(setmetatable, f, mt)
--[==[
-- If it's protected, then `mt` could be anything, but use the heuristic that if a `__call` key exists then that's probably intentional.
A special form of {pcall()}, which returns {true} plus the result value(s) if {f} is callable, or {false} if it isn't. Errors that occur within the called function are not protected.]==]
-- This also builds in ways to ensure that this function always returns the correct result when implementing protected metatables.
function export.try_call(f, ...)
if not success then
local callable = is_callable(f, true)
if type(mt) ~= "table" then
if callable then
return true, f(...)
elseif callable == false then
return false
return false
end
end
local __metatable = rawget(mt, "__metatable")
-- If `callable` is nil, there's a protected metatable, so there's no way to check without doing a protected call.
-- If the value of `__metatable` is also `mt`, then `mt` must be the true metatable anyway (e.g. mw.loadData does this).
return catch_values(f, pcall(f, ...))
end
end
local __call = rawget(mt, "__call")
-- `__call` metamethods have to be functions, so don't recurse when checking it.
return __call ~= nil and type(__call) == "function"
end
end


--[==[
Takes two or more functions as arguments, and returns a new function which calls each of the input functions in turn. Any arguments given to the returned function are given to the first function, and all other functions receive the output value(s) from the previous function.]==]
function export.chain(func1, func2, ...)
function export.chain(func1, func2, ...)
return func1(func2(...))
local function chained_func(...)
return func2(func1(...))
end
if select("#", ...) == 0 then
return chained_func
end
return chain(chained_func, ...)
end
chain = export.chain
 
--[==[
Takes the usual for-loop parameters (an iterator, plus an optional state and initial index), and unrolls the iterator by returning every (first) value returned by the iterator.
 
For instance, {unroll(pairs(t))} will return every key in {t}, and {unroll(string.gmatch(s, "%w+"))} will return every word in {s}.]==]
function export.unroll(iter, state, k)
k = iter(state, k)
if k ~= nil then
return k, unroll(iter, state, k)
end
end
unroll = export.unroll
 
--[==[
Takes a generator function (i.e. a function that returns an iterator, such as {ipairs}) and one or more additional functions, and returns a new generator function. Any arguments given to the new generator (e.g. an input table) are given to the original generator, and the additional functions are called on each iteration. The first additional function takes the output from the original iterator (i.e. the function returned by the original generator), and any further functions receive the output value(s) from the previous function. This can be used to modify the values returned from an iterator.]==]
function export.chainIter(gen, new_iter, ...)
if select("#", ...) > 0 then
new_iter = chain(new_iter, ...)
end
return function(...)
local orig_iter, state, k = gen(...)
-- k has to be the first value returned by orig_iter on the last iteration, not whatever new_iter returned.
local function catch_values(...)
k = ...
if k ~= nil then
return new_iter(...)
end
end
return function()
return catch_values(orig_iter(state, k))
end, state, k
end
end
end
chain_iter = export.chainIter


do
do
Line 272: Line 370:
function export.logAll(t)
function export.logAll(t)
for k, v in pairs(t) do
for k, v in pairs(t) do
if type(v) == "function" then
if is_callable(v) then
t[k] = export.logReturnValues(v, tostring(k))
t[k] = export.logReturnValues(v, tostring(k))
end
end
Anonymous user