--[[ functional programming facilities be wary of use in performance critical code under luajit absolute performance is this module's achilles heel; you're generally allocating more garbage than is strictly necessary, plus inline anonymous will be re-created each call, which is NYI this can be a Bad Thing and means probably this isn't a great module to heavily leverage in the middle of your action game's physics update but, there are many cases where it matters less than you'd think generally, if it wasn't hot enough to get compiled anyway, you're fine (if all this means nothing to you, just don't worry about it) ]] local path = (...):gsub("functional", "") local tablex = require(path .. "tablex") local mathx = require(path .. "mathx") local functional = setmetatable({}, { __index = tablex, }) --the identity function function functional.identity(v) return v end --simple sequential iteration, f is called for all elements of t --f can return non-nil to break the loop (and return the value) --otherwise returns t for chaining function functional.foreach(t, f) for i = 1, #t do local result = f(t[i], i) if result ~= nil then return result end end return t end --performs a left to right reduction of t using f, with seed as the initial value -- reduce({1, 2, 3}, 0, f) -> f(f(f(0, 1), 2), 3) -- (but performed iteratively, so no stack smashing) function functional.reduce(t, seed, f) for i = 1, #t do seed = f(seed, t[i], i) end return seed end --maps a sequence {a, b, c} -> {f(a), f(b), f(c)} -- (automatically drops any nils to keep a sequence, so can be used to simultaneously map and filter) function functional.map(t, f) local result = {} for i = 1, #t do local v = f(t[i], i) if v ~= nil then table.insert(result, v) end end return result end --maps a sequence inplace, modifying it {a, b, c} -> {f(a), f(b), f(c)} -- (automatically drops any nils, which can be used to simultaneously map and filter) function functional.map_inplace(t, f) local write_i = 0 local n = #t --cache, so splitting the sequence doesn't stop iteration for i = 1, n do local v = f(t[i], i) if v ~= nil then write_i = write_i + 1 t[write_i] = v end if i ~= write_i then t[i] = nil end end return t end --alias functional.remap = functional.map_inplace --maps a sequence {a, b, c} -> {a[k], b[k], c[k]} -- (automatically drops any nils to keep a sequence) function functional.map_field(t, k) local result = {} for i = 1, #t do local v = t[i][k] if v ~= nil then table.insert(result, v) end end return result end --maps a sequence by a method call -- if m is a string method name like "position", {a, b} -> {a:m(...), b:m(...)} -- if m is function reference like player.get_position, {a, b} -> {m(a, ...), m(b, ...)} -- (automatically drops any nils to keep a sequence) function functional.map_call(t, m, ...) local result = {} for i = 1, #t do local v = t[i] local f = type(m) == "function" and m or v[m] v = f(v, ...) if v ~= nil then table.insert(result, v) end end return result end --maps a sequence into a new index space (see functional.map) -- the function may return an index where the value will be stored in the result -- if no index (or a nil index) is provided, it will insert as normal function functional.splat(t, f) local result = {} for i = 1, #t do local v, pos = f(t[i], i) if v ~= nil then if pos == nil then pos = #result + 1 end result[pos] = v end end return result end --filters a sequence -- returns a table containing items where f(v, i) returns truthy function functional.filter(t, f) local result = {} for i = 1, #t do local v = t[i] if f(v, i) then table.insert(result, v) end end return result end --filters a sequence in place, modifying it function functional.filter_inplace(t, f) local write_i = 0 local n = #t --cache, so splitting the sequence doesn't stop iteration for i = 1, n do local v = t[i] if f(v, i) then write_i = write_i + 1 t[write_i] = v end if i ~= write_i then t[i] = nil end end return t end -- complement of filter -- returns a table containing items where f(v) returns falsey -- nil results are included so that this is an exact complement of filter; consider using partition if you need both! function functional.remove_if(t, f) local result = {} for i = 1, #t do local v = t[i] if not f(v, i) then table.insert(result, v) end end return result end --partitions a sequence into two, based on filter criteria --simultaneous filter and remove_if function functional.partition(t, f) local a = {} local b = {} for i = 1, #t do local v = t[i] if f(v, i) then table.insert(a, v) else table.insert(b, v) end end return a, b end -- returns a table where the elements in t are grouped into sequential tables by the result of f on each element. -- more general than partition, but requires you to know your groups ahead of time -- (or use numeric grouping and pre-seed) if you want to avoid pairs! function functional.group_by(t, f) local result = {} for i = 1, #t do local v = t[i] local group = f(v, i) if result[group] == nil then result[group] = {} end table.insert(result[group], v) end return result end --combines two same-length sequences through a function f -- f receives arguments (t1[i], t2[i], i) -- iteration limited by min(#t1, #t2) -- ignores nil results function functional.combine(t1, t2, f) local ret = {} local limit = math.min(#t1, #t2) for i = 1, limit do local v1 = t1[i] local v2 = t2[i] local zipped = f(v1, v2, i) if zipped ~= nil then table.insert(ret, zipped) end end return ret end --zips two sequences together into a new table, alternating from t1 and t2 -- zip({1, 2}, {3, 4}) -> {1, 3, 2, 4} -- iteration limited by min(#t1, #t2) function functional.zip(t1, t2) local ret = {} local limit = math.min(#t1, #t2) for i = 1, limit do table.insert(ret, t1[i]) table.insert(ret, t2[i]) end return ret end --unzips a table into two new tables, alternating elements into each result -- {1, 2, 3, 4} -> {1, 3}, {2, 4} -- gets an extra result in the first result for odd-length tables function functional.unzip(t) local a = {} local b = {} for i, v in ipairs(t) do table.insert(i % 2 == 1 and a or b, v) end return a, b end ----------------------------------------------------------- --specialised maps -- (experimental: let me know if you have better names for these!) ----------------------------------------------------------- --maps a sequence {a, b, c} -> collapse { f(a), f(b), f(c) } -- (ie results from functions should generally be sequences, -- which are appended onto each other, resulting in one big sequence) -- (automatically drops any nils, same as map) function functional.stitch(t, f) local result = {} for i, v in ipairs(t) do v = f(v, i) if v ~= nil then if type(v) == "table" then for _, e in ipairs(v) do table.insert(result, e) end else table.insert(result, v) end end end return result end --alias functional.map_stitch = functional.stitch --maps a sequence {a, b, c} -> { f(a, b), f(b, c), f(c, a) } -- useful for inter-dependent data -- (automatically drops any nils, same as map) function functional.cycle(t, f) local result = {} for i, a in ipairs(t) do local b = t[mathx.wrap(i + 1, 1, #t + 1)] local v = f(a, b) if v ~= nil then table.insert(result, v) end end return result end functional.map_cycle = functional.cycle --maps a sequence {a, b, c} -> { f(a, b), f(b, c) } -- useful for inter-dependent data -- (automatically drops any nils, same as map) function functional.chain(t, f) local result = {} for i = 2, #t do local a = t[i-1] local b = t[i] local v = f(a, b) if v ~= nil then table.insert(result, v) end end return result end functional.map_chain = functional.chain --maps a sequence {a, b, c, d} -> { f(a, b), f(a, c), f(a, d), f(b, c), f(b, d), f(c, d) } -- ie all distinct pairs are mapped, useful for any N^2 dataset (eg finding neighbours) function functional.map_pairs(t, f) local result = {} for i = 1, #t do for j = i+1, #t do local a = t[i] local b = t[j] local v = f(a, b) if v ~= nil then table.insert(result, v) end end end return result end ----------------------------------------------------------- --generating data ----------------------------------------------------------- --generate data into a table --basically a map on numeric values from 1 to count --nil values are omitted in the result, as for map function functional.generate(count, f) local result = {} for i = 1, count do local v = f(i) if v ~= nil then table.insert(result, v) end end return result end --2d version of the above --note: ends up with a 1d table; -- if you need a 2d table, you should nest 1d generate calls function functional.generate_2d(width, height, f) local result = {} for y = 1, height do for x = 1, width do local v = f(x, y) if v ~= nil then table.insert(result, v) end end end return result end ----------------------------------------------------------- --common queries and reductions ----------------------------------------------------------- --true if any element of the table matches f function functional.any(t, f) for i = 1, #t do if f(t[i], i) then return true end end return false end --true if no element of the table matches f function functional.none(t, f) for i = 1, #t do if f(t[i], i) then return false end end return true end --true if all elements of the table match f function functional.all(t, f) for i = 1, #t do if not f(t[i], i) then return false end end return true end --counts the elements of t that match f function functional.count(t, f) local c = 0 for i = 1, #t do if f(t[i], i) then c = c + 1 end end return c end --counts the elements of t equal to v function functional.count_value(t, v) local c = 0 for i = 1, #t do if t[i] == v then c = c + 1 end end return c end --true if the table contains element e function functional.contains(t, e) for i = 1, #t do if t[i] == e then return true end end return false end --return the numeric sum of all elements of t function functional.sum(t) local c = 0 for i = 1, #t do c = c + t[i] end return c end --return the numeric mean of all elements of t function functional.mean(t) local len = #t if len == 0 then return 0 end return functional.sum(t) / len end --return the minimum and maximum of t in one pass --or zero for both if t is empty -- (would perhaps more correctly be math.huge, -math.huge -- but that tends to be surprising/annoying in practice) function functional.minmax(t) local n = #t if n == 0 then return 0, 0 end local max = t[1] local min = t[1] for i = 2, n do local v = t[i] min = math.min(min, v) max = math.max(max, v) end return min, max end --return the maximum element of t or zero if t is empty function functional.max(t) local min, max = functional.minmax(t) return max end --return the minimum element of t or zero if t is empty function functional.min(t) local min, max = functional.minmax(t) return min end --return the element of the table that results in the lowest numeric value --(function receives element and index respectively) function functional.find_min(t, f) local current = nil local current_min = math.huge for i = 1, #t do local e = t[i] local v = f(e, i) if v and v < current_min then current_min = v current = e end end return current end --return the element of the table that results in the greatest numeric value --(function receives element and index respectively) function functional.find_max(t, f) local current = nil local current_max = -math.huge for i = 1, #t do local e = t[i] local v = f(e, i) if v and v > current_max then current_max = v current = e end end return current end --alias functional.find_best = functional.find_max --return the element of the table that results in the value nearest to the passed value --todo: optimise, inline as this generates a closure each time function functional.find_nearest(t, f, target) local current = nil local current_min = math.huge for i = 1, #t do local e = t[i] local v = math.abs(f(e, i) - target) if v and v < current_min then current_min = v current = e if v == 0 then break end end end return current end --return the first element of the table that results in a true filter function functional.find_match(t, f) for i = 1, #t do local v = t[i] if f(v) then return v end end return nil end return functional