Module:fun: Difference between revisions
Jump to navigation
Jump to search
No edit summary |
m (1 revision imported) |
||
| (One intermediate revision by one other user not shown) | |||
| Line 1: | Line 1: | ||
local export = {} | local export = {} | ||
local | 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 rawget = rawget | local rawget = rawget | ||
local require = require | |||
local select = select | local select = select | ||
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(...) | |||
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 ( | 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 | 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) | ||
if type(f) == "function" then | |||
return true | return true | ||
end | end | ||
-- | -- 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 = | 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 | ||
-- | |||
--[==[ | |||
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.]==] | |||
function export.try_call(f, ...) | |||
local callable = is_callable(f, true) | |||
if callable then | |||
return true, f(...) | |||
elseif callable == false then | |||
return false | return false | ||
end | end | ||
-- If `callable` is nil, there's a protected metatable, so there's no way to check without doing a protected call. | |||
-- If | return catch_values(f, pcall(f, ...)) | ||
end | end | ||
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( | 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 | if is_callable(v) then | ||
t[k] = export.logReturnValues(v, tostring(k)) | t[k] = export.logReturnValues(v, tostring(k)) | ||
end | end | ||
Latest revision as of 17:47, 4 November 2025
- The following documentation is located at Module:fun/doc.[edit]
- Useful links: subpage list • links • transclusions • testcases • sandbox
local export = {}
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 ipairs = ipairs
local is_callable -- defined below
local pairs = pairs
local pcall = pcall
local rawget = rawget
local require = require
local select = select
local tostring = tostring
local type = type
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
local unroll -- defined below
local xpcall = xpcall
local function debug_track(...)
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)
i = i + 1
local char = iter()
if char ~= nil then
return i, char
end
end
-- Iterate over UTF-8-encoded codepoints in string.
local function iterString(str)
return _iterString, gmatch(str, ".[\128-\191]*"), 0
end
--[==[
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 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, allow_maybe)
if type(f) == "function" then
return true
end
-- 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 = get_unprotected_metatable(f)
if mt == nil then
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
--[==[
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.]==]
function export.try_call(f, ...)
local callable = is_callable(f, true)
if callable then
return true, f(...)
elseif callable == false then
return false
end
-- If `callable` is nil, there's a protected metatable, so there's no way to check without doing a protected call.
return catch_values(f, pcall(f, ...))
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, ...)
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
chain_iter = export.chainIter
do
local function catch_values(start, iter, state, k, ...)
if start == k or k == nil then
return k, ...
end
return catch_values(start, iter, state, iter(state, k))
end
function export.iterateFrom(start, iter, state, k)
local first = true
return function(state, k)
if first then
first = false
return catch_values(start, iter, state, iter(state, k))
end
return iter(state, k)
end, state, k
end
end
-- map(function(number) return number ^ 2 end,
-- { 1, 2, 3 }) --> { 1, 4, 9 }
-- map(function (char) return string.char(string.byte(char) - 0x20) end,
-- "abc") --> { "A", "B", "C" }
function export.map(func, iterable, isArray)
local array = {}
for k, v in (type(iterable) == "string" and iterString or (isArray or iterable[1] ~= nil) and ipairs or pairs)(iterable) do
array[k] = func(v, k, iterable)
end
return array
end
function export.mapIter(func, iter, state, init)
-- init could be anything
local array, i = {}, 0
for x, y in iter, state, init do
i = i + 1
array[i] = func(y, x, state)
end
return array
end
do
local function iter_tuples(tuples)
local i = tuples.i
if i > 1 then
i = i - 1
tuples.i = i
return unpack(tuples[i])
end
end
-- Takes an iterator function, and returns a new iterator that iterates in reverse, given the same arguments.
-- Note: changes to the state during iteration are not taken into account, since all the return values are calculated in advance.
function export.reverseIter(func)
return function(...)
-- Store all returned values as a list of tuples, then iterate in reverse over that list.
local tuples, i, iter, state, val1 = {}, 0, func(...)
while true do
i = i + 1
local vals = {iter(state, val1)}
-- Terminates if the first return value is nil, even if other values are non-nil.
val1 = vals[1]
if val1 == nil then
tuples.i = i
return iter_tuples, tuples
end
tuples[i] = vals
end
end
end
end
function export.forEach(func, iterable, isArray)
for k, v in (type(iterable) == "string" and iterString or (isArray or iterable[1] ~= nil) and ipairs or pairs)(iterable) do
func(v, k, iterable)
end
return nil
end
-------------------------------------------------
-- From http://lua-users.org/wiki/CurriedLua
-- reverse(...) : take some tuple and return a tuple of elements in reverse order
--
-- e.g. "reverse(1,2,3)" returns 3,2,1
local function reverse(...)
-- reverse args by building a function to do it, similar to the unpack() example
local function reverseHelper(acc, v, ...)
if select("#", ...) == 0 then
return v, acc()
else
return reverseHelper(function() return v, acc() end, ...)
end
end
-- initial acc is the end of the list
return reverseHelper(function() return end, ...)
end
function export.curry(func, numArgs)
-- currying 2-argument functions seems to be the most popular application
numArgs = numArgs or 2
-- no sense currying for 1 arg or less
if numArgs <= 1 then return func end
-- helper takes an argTrace function, and number of arguments remaining to be applied
local function curryHelper(argTrace, n)
if n == 0 then
-- kick off argTrace, reverse argument list, and call the original function
return func(reverse(argTrace()))
else
-- "push" argument (by building a wrapper function) and decrement n
return function(onearg)
return curryHelper(function() return onearg, argTrace() end, n - 1)
end
end
end
-- push the terminal case of argTrace into the function first
return curryHelper(function() return end, numArgs)
end
-------------------------------------------------
-- some(function(val) return val % 2 == 0 end,
-- { 2, 3, 5, 7, 11 }) --> true
function export.some(func, t, isArray)
for k, v in ((isArray or t[1] ~= nil) and ipairs or pairs)(t) do
if func(v, k, t) then
return true
end
end
return false
end
-- all(function(val) return val % 2 == 0 end,
-- { 2, 4, 8, 10, 12 }) --> true
function export.all(func, t, isArray)
for k, v in ((isArray or t[1] ~= nil) and ipairs or pairs)(t) do
if not func(v, k, t) then
return false
end
end
return true
end
function export.filter(func, t, isArray)
local new_t = {}
if isArray or t[1] ~= nil then -- array
local new_i = 0
for i, v in ipairs(t) do
if func(v, i, t) then
new_i = new_i + 1
new_t[new_i] = v
end
end
else
for k, v in pairs(t) do
if func(v, k, t) then
new_t[k] = v -- or create array?
end
end
end
return new_t
end
function export.fold(func, t, accum)
for i, v in ipairs(t) do
accum = func(accum, v, i, t)
end
return accum
end
-------------------------------
-- Fancy stuff
local function capture(...)
local vals = {n = select("#", ...), ...}
return function()
return unpack(vals, 1, vals.n)
end
end
-- Log input and output of function.
-- Receives a function and returns a modified form of that function.
function export.logReturnValues(func, prefix)
return function(...)
local inputValues = capture(...)
local returnValues = capture(func(...))
if prefix then
mw.log(prefix, inputValues())
mw.log(returnValues())
else
mw.log(inputValues())
mw.log(returnValues())
end
return returnValues()
end
end
export.log = export.logReturnValues
-- Convenience function to make all functions in a table log their input and output.
function export.logAll(t)
for k, v in pairs(t) do
if is_callable(v) then
t[k] = export.logReturnValues(v, tostring(k))
end
end
return t
end
return export