batteries/functional.lua
2020-05-13 21:09:15 +10:00

330 lines
7.3 KiB
Lua

--[[
functional programming facilities
notes:
be careful about creating closures in hot loops.
this is this module's achilles heel - there's no special
syntax for closures so it's not apparent that you're suddenly
allocating at every call
reduce has a similar problem, but at least arguments
there are clear!
]]
local path = (...):gsub("functional", "")
local tablex = require(path .. "tablex")
local functional = setmetatable({}, {
__index = tablex,
})
--simple sequential iteration, f is called for all elements of t
--f can return non-nil to break the loop (and return the value)
function functional.foreach(t, f)
for i,v in ipairs(t) do
local r = f(v, i)
if r ~= nil then
return r
end
end
end
--performs a left to right reduction of t using f, with o as the initial value
-- reduce({1, 2, 3}, f, 0) -> f(f(f(0, 1), 2), 3)
-- (but performed iteratively, so no stack smashing)
function functional.reduce(t, f, o)
for i,v in ipairs(t) do
o = f(o, v)
end
return o
end
--maps a sequence {a, b, c} -> {f(a), f(b), f(c)}
-- (automatically drops any nils due to table.insert, which can be used to simultaneously map and filter)
function functional.map(t, f)
local r = {}
for i,v in ipairs(t) do
local mapped = f(v, i)
if mapped ~= nil then
table.insert(r, mapped)
end
end
return r
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,
-- but this results in a linear table.remove so "careful" for big working sets)
function functional.remap(t, f)
local i = 1
while i <= #t do
local mapped = f(t[i])
if mapped ~= nil then
t[i] = mapped
i = i + 1
else
table.remove(t, i)
end
end
return t
end
--filters a sequence
-- returns a table containing items where f(v) returns truthy
function functional.filter(t, f)
local r = {}
for i,v in ipairs(t) do
if f(v, i) then
table.insert(r, v)
end
end
return r
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 r = {}
for i, v in ipairs(t) do
if not f(v, i) then
table.insert(r, v)
end
end
return r
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,v in ipairs(t) do
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) if you want to avoid pairs!
function functional.group_by(t, f)
local result = {}
for i, v in ipairs(t) do
local group = f(v)
if result[group] == nil then
result[group] = {}
end
table.insert(result[group], v)
end
return result
end
--zips two sequences together into a new table, based on another function
--iteration limited by min(#t1, #t2)
--function receives arguments (t1, t2, i)
--nil results ignored
function functional.zip(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
-----------------------------------------------------------
--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 r = {}
for i = 1, count do
local v = f(i)
if v ~= nil then
table.insert(r, v)
end
end
return r
end
--2d version of the above
--note: ends up with a 1d table;
-- if you need a 2d table, nest 1d generate calls
function functional.generate_2d(width, height, f)
local r = {}
for y = 1, height do
for x = 1, width do
local v = f(x, y)
if v ~= nil then
table.insert(r, v)
end
end
end
return r
end
-----------------------------------------------------------
--common queries and reductions
-----------------------------------------------------------
--true if any element of the table matches f
function functional.any(t, f)
for i,v in ipairs(t) do
if f(v) then
return true
end
end
return false
end
--true if no element of the table matches f
function functional.none(t, f)
for i,v in ipairs(t) do
if f(v) then
return false
end
end
return true
end
--true if all elements of the table match f
function functional.all(t, f)
for i,v in ipairs(t) do
if not f(v) 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,v in ipairs(t) do
if f(v) then
c = c + 1
end
end
return c
end
--true if the table contains element e
function functional.contains(t, e)
for i, v in ipairs(t) do
if v == e then
return true
end
end
return false
end
--return the numeric sum of all elements of t
function functional.sum(t)
return functional.reduce(t, function(a, b)
return a + b
end, 0)
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 max, min
for i,v in ipairs(t) do
min = not min and v or math.min(min, v)
max = not max and v or math.max(max, v)
end
if min == nil then
min = 0
max = 0
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, e in ipairs(t) do
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, e in ipairs(t) do
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 as this generates a closure each time
function functional.find_nearest(t, f, v)
return functional.find_min(t, function(e)
return math.abs(f(e) - v)
end)
end
--return the first element of the table that results in a true filter
function functional.find_match(t, f)
for i,v in ipairs(t) do
if f(v) then
return v
end
end
return nil
end
return functional