[modified] moderate functional refactor (BREAKING)

- modified functional.reduce signature; takes seed value before function now as this results in much more readable calling code
- added filter_inplace and aliased remap as map_inplace to make semantics more clear
- rewrote many functions to use numeric for as a tiny speed boost at the cost of library code readability
[modified] sequence to be compatible
This commit is contained in:
Max Cahill 2020-11-10 20:51:43 +11:00
parent 1eaf77ad7a
commit 715765003d
2 changed files with 124 additions and 74 deletions

View File

@ -26,61 +26,86 @@ end
--simple sequential iteration, f is called for all elements of t --simple sequential iteration, f is called for all elements of t
--f can return non-nil to break the loop (and return the value) --f can return non-nil to break the loop (and return the value)
function functional.foreach(t, f) function functional.foreach(t, f)
for i,v in ipairs(t) do local n = #t
local r = f(v, i) for i = 1, n do
if r ~= nil then local result = f(t[i], i)
return r if result ~= nil then
return result
end end
end end
end end
--performs a left to right reduction of t using f, with o as the initial value --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) -- reduce({1, 2, 3}, 0, f) -> f(f(f(0, 1), 2), 3)
-- (but performed iteratively, so no stack smashing) -- (but performed iteratively, so no stack smashing)
function functional.reduce(t, f, o) function functional.reduce(t, seed, f)
for i,v in ipairs(t) do local n = #t
o = f(o, v) for i = 1, n do
seed = f(seed, t[i], i)
end end
return o return seed
end end
--maps a sequence {a, b, c} -> {f(a), f(b), f(c)} --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) -- (automatically drops any nils to keep a sequence, so can be used to simultaneously map and filter)
function functional.map(t, f) function functional.map(t, f)
local r = {} local result = {}
for i,v in ipairs(t) do local n = #t
local mapped = f(v, i) for i = 1, n do
if mapped ~= nil then local v = f(t[i], i)
table.insert(r, mapped) if v ~= nil then
table.insert(result, v)
end end
end end
return r return result
end end
--maps a sequence inplace, modifying it {a, b, c} -> {f(a), f(b), f(c)} --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, -- (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.map_inplace(t, f)
function functional.remap(t, f) local write_i = 0
local i = 1 local n = #t
while i <= #t do for i = 1, n do
local mapped = f(t[i]) local v = f(t[i], i)
if mapped ~= nil then if v ~= nil then
t[i] = mapped write_i = write_i + 1
i = i + 1 t[write_i] = v
else end
table.remove(t, i) if i ~= write_i then
t[i] = nil
end end
end end
return t return t
end end
--alias
functional.remap = functional.map_inplace
--filters a sequence --filters a sequence
-- returns a table containing items where f(v) returns truthy -- returns a table containing items where f(v, i) returns truthy
function functional.filter(t, f) function functional.filter(t, f)
local r = {} local result = {}
for i,v in ipairs(t) do local n = #t
for i = 1, n do
if f(t[i], 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 = 1
local n = #t
for i = 1, n do
local v = t[i]
if f(v, i) then if f(v, i) then
table.insert(r, v) t[write_i] = v
write_i = write_i + 1
end
if i ~= write_i then
t[i] = nil
end end
end end
return r return r
@ -90,13 +115,14 @@ end
-- returns a table containing items where f(v) returns falsey -- 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! -- 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) function functional.remove_if(t, f)
local r = {} local result = {}
for i, v in ipairs(t) do local n = #t
if not f(v, i) then for i = 1, n do
table.insert(r, v) if not f(t[i], i) then
table.insert(result, v)
end end
end end
return r return result
end end
--partitions a sequence into two, based on filter criteria --partitions a sequence into two, based on filter criteria
@ -104,8 +130,9 @@ end
function functional.partition(t, f) function functional.partition(t, f)
local a = {} local a = {}
local b = {} local b = {}
for i,v in ipairs(t) do local n = #t
if f(v, i) then for i = 1, n do
if f(t[i], i) then
table.insert(a, v) table.insert(a, v)
else else
table.insert(b, v) table.insert(b, v)
@ -115,11 +142,13 @@ function functional.partition(t, f)
end end
-- returns a table where the elements in t are grouped into sequential tables by the result of f on each element. -- 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! -- 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) function functional.group_by(t, f)
local result = {} local result = {}
for i, v in ipairs(t) do local n = #t
local group = f(v) for i = 1, n do
local group = f(t[i], i)
if result[group] == nil then if result[group] == nil then
result[group] = {} result[group] = {}
end end
@ -154,26 +183,26 @@ end
--basically a map on numeric values from 1 to count --basically a map on numeric values from 1 to count
--nil values are omitted in the result, as for map --nil values are omitted in the result, as for map
function functional.generate(count, f) function functional.generate(count, f)
local r = {} local result = {}
for i = 1, count do for i = 1, count do
local v = f(i) local v = f(i)
if v ~= nil then if v ~= nil then
table.insert(r, v) table.insert(result, v)
end end
end end
return r return result
end end
--2d version of the above --2d version of the above
--note: ends up with a 1d table; --note: ends up with a 1d table;
-- if you need a 2d table, you should nest 1d generate calls -- if you need a 2d table, you should nest 1d generate calls
function functional.generate_2d(width, height, f) function functional.generate_2d(width, height, f)
local r = {} local result = {}
for y = 1, height do for y = 1, height do
for x = 1, width do for x = 1, width do
local v = f(x, y) local v = f(x, y)
if v ~= nil then if v ~= nil then
table.insert(r, v) table.insert(result, v)
end end
end end
end end
@ -186,8 +215,9 @@ end
--true if any element of the table matches f --true if any element of the table matches f
function functional.any(t, f) function functional.any(t, f)
for i,v in ipairs(t) do local n = #t
if f(v) then for i = 1, n do
if f(t[i], i) then
return true return true
end end
end end
@ -196,8 +226,9 @@ end
--true if no element of the table matches f --true if no element of the table matches f
function functional.none(t, f) function functional.none(t, f)
for i,v in ipairs(t) do local n = #t
if f(v) then for i = 1, n do
if f(t[i], i) then
return false return false
end end
end end
@ -206,8 +237,9 @@ end
--true if all elements of the table match f --true if all elements of the table match f
function functional.all(t, f) function functional.all(t, f)
for i,v in ipairs(t) do local n = #t
if not f(v) then for i = 1, n do
if not f(t[i], i) then
return false return false
end end
end end
@ -217,8 +249,9 @@ end
--counts the elements of t that match f --counts the elements of t that match f
function functional.count(t, f) function functional.count(t, f)
local c = 0 local c = 0
for i,v in ipairs(t) do local n = #t
if f(v) then for i = 1, n do
if f(t[i], i) then
c = c + 1 c = c + 1
end end
end end
@ -227,8 +260,9 @@ end
--true if the table contains element e --true if the table contains element e
function functional.contains(t, e) function functional.contains(t, e)
for i, v in ipairs(t) do local n = #t
if v == e then for i = 1, n do
if t[i] == e then
return true return true
end end
end end
@ -237,9 +271,12 @@ end
--return the numeric sum of all elements of t --return the numeric sum of all elements of t
function functional.sum(t) function functional.sum(t)
return functional.reduce(t, function(a, b) local c = 0
return a + b local n = #t
end, 0) for i = 1, n do
c = c + t[i]
end
return c
end end
--return the numeric mean of all elements of t --return the numeric mean of all elements of t
@ -256,14 +293,15 @@ end
-- (would perhaps more correctly be math.huge, -math.huge -- (would perhaps more correctly be math.huge, -math.huge
-- but that tends to be surprising/annoying in practice) -- but that tends to be surprising/annoying in practice)
function functional.minmax(t) function functional.minmax(t)
local max, min local n = #t
for i,v in ipairs(t) do if n == 0 then
min = not min and v or math.min(min, v) return 0, 0
max = not max and v or math.max(max, v)
end end
if min == nil then local max = t[1]
min = 0 local min = t[1]
max = 0 for i = 2, n do
min = math.min(min, v)
max = math.max(max, v)
end end
return min, max return min, max
end end
@ -285,7 +323,9 @@ end
function functional.find_min(t, f) function functional.find_min(t, f)
local current = nil local current = nil
local current_min = math.huge local current_min = math.huge
for i, e in ipairs(t) do local n = #t
for i = 1, n do
local e = t[i]
local v = f(e, i) local v = f(e, i)
if v and v < current_min then if v and v < current_min then
current_min = v current_min = v
@ -300,7 +340,9 @@ end
function functional.find_max(t, f) function functional.find_max(t, f)
local current = nil local current = nil
local current_max = -math.huge local current_max = -math.huge
for i, e in ipairs(t) do local n = #t
for i = 1, n do
local e = t[i]
local v = f(e, i) local v = f(e, i)
if v and v > current_max then if v and v > current_max then
current_max = v current_max = v
@ -314,7 +356,7 @@ end
functional.find_best = functional.find_max functional.find_best = functional.find_max
--return the element of the table that results in the value nearest to the passed value --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 --todo: optimise, inline as this generates a closure each time
function functional.find_nearest(t, f, v) function functional.find_nearest(t, f, v)
return functional.find_min(t, function(e) return functional.find_min(t, function(e)
return math.abs(f(e) - v) return math.abs(f(e) - v)
@ -323,7 +365,9 @@ end
--return the first element of the table that results in a true filter --return the first element of the table that results in a true filter
function functional.find_match(t, f) function functional.find_match(t, f)
for i,v in ipairs(t) do local n = #t
for i = 1, n do
local v = t[i]
if f(v) then if f(v) then
return v return v
end end

View File

@ -58,22 +58,28 @@ function sequence:foreach(f)
return functional.foreach(self, f) return functional.foreach(self, f)
end end
function sequence:reduce(f, o) function sequence:reduce(seed, f)
return functional.reduce(self, f, o) return functional.reduce(self, seed, f)
end end
function sequence:map(f) function sequence:map(f)
return sequence(functional.map(self, f)) return sequence(functional.map(self, f))
end end
function sequence:remap(f) function sequence:map_inplace(f)
return functional.remap(self, f) return sequence(functional.map_inplace(self, f))
end end
sequence.remap = sequence.map_inplace
function sequence:filter(f) function sequence:filter(f)
return sequence(functional.filter(self, f)) return sequence(functional.filter(self, f))
end end
function sequence:filter_inplace(f)
return sequence(functional.filter_inplace(self, f))
end
function sequence:remove_if(f) function sequence:remove_if(f)
return sequence(functional.remove_if(self, f)) return sequence(functional.remove_if(self, f))
end end