xllify now ships a functional global in every Lua state. This post explains what is in it, why it exists, and when it makes your code substantially cleaner.
What is in it
-- higher-order
functional.map(t, f) -- transform every element
functional.filter(t, f) -- keep elements matching a predicate
functional.reduce(t, f) -- fold from first element
functional.fold(t, f, acc) -- fold with an explicit seed
functional.zip(a, b, f) -- combine two tables element-wise
functional.flatten(t, depth) -- collapse nested tables
functional.flat_map(t, f) -- map then flatten
functional.compose(...) -- right-to-left function composition
functional.pipe(...) -- left-to-right function composition
functional.partial(f, ...) -- bind leading arguments
-- predicates
functional.every(t, f) -- true if f returns true for all elements
functional.some(t, f) -- true if f returns true for any element
functional.none(t, f) -- true if f returns true for no elements
-- grouping and counting
functional.group_by(t, f) -- group elements into sub-tables by key function
functional.frequencies(t) -- count occurrences of each value
-- slicing
functional.take_while(t, f) -- take elements from the front while predicate holds
functional.drop_while(t, f) -- drop elements from the front while predicate holds
functional.distinct(t) -- remove duplicate values
-- misc
functional.juxt(...) -- apply multiple functions to the same argument, return results as table
functional.const(v) -- returns a function that always returns v
-- named numeric utilities (pass directly to map, filter, etc.)
functional.double functional.square functional.sqrt
functional.negate functional.abs functional.round
functional.floor functional.ceil functional.inc
functional.dec functional.half functional.identity
-- arithmetic combinators (each returns a function)
functional.add(n) functional.sub(n) functional.mul(n)
functional.div(n) functional.pow(n) functional.mod(n)
All of them take plain functions as arguments. Sadly, Lua has no arrow syntax; function(x) return ... end is the only form. The named utilities and combinators above exist specifically to reduce how often you need to write that. functional.mul(2) is shorter and more readable than function(x) return x * 2 end.
Why loop when you can pipeline
Here is the sort of thing that shows up constantly in Excel custom functions: take a range, filter out blanks and negatives, sum what is left.
The loop version:
--- Sum only positive values in a range
-- @param values number Values to sum (matrix)
function SumPositive(values)
local total = 0
for _, row in ipairs(values) do
for _, v in ipairs(row) do
if type(v) == "number" and v > 0 then
total = total + v
end
end
end
return total
end
The functional version:
--- Sum only positive values in a range
-- @param values number Values to sum (matrix)
function SumPositive(values)
local function isPositive(v) return v > 0 end
local function sum(acc, v) return acc + v end
local flat = xllify.flatten_range(values) -- 1D, non-numbers already stripped
local positive = functional.filter(flat, isPositive)
return functional.fold(positive, sum, 0)
end
Same result. The second version reads as a description of what is happening rather than a description of how to iterate. When you come back to it three months later you do not have to trace the loop structure to understand the intent. It’s a matter of personal preference which works best for you.
For JavaScript developers
If you write array.map, array.filter, and array.reduce habitually, this will feel immediately familiar. The main differences from JavaScript:
functional.map(t, f)rather thant.map(f): the functions are not methods on the tablefunction(x) return x * 2 endrather thanx => x * 2(this is unfortunate as Lua has no arrow functions 😢)- Tables are 1-indexed
Everything else translates directly. flat_map is the Lua equivalent of Array.prototype.flatMap.
-- JavaScript: const f = x => (x * 2) + 1
-- Lua:
local f = functional.pipe(functional.double, functional.inc)
f(5) -- 11
-- JavaScript: [1,2,3,4,5].filter(x => x % 2 === 0).map(x => x * x)
-- Lua:
local function isEven(x) return x % 2 == 0 end
local result = functional.map(
functional.filter({ 1, 2, 3, 4, 5 }, isEven),
functional.square
)
-- { 4, 16 }
For Python developers
Python’s map, filter, and functools.reduce are direct analogues. The mental model is the same. partial behaves like functools.partial.
-- Python: list(map(lambda x: x ** 2, [1, 2, 3, 4, 5]))
-- Lua:
local squares = functional.map({ 1, 2, 3, 4, 5 }, function(x) return x * x end)
-- { 1, 4, 9, 16, 25 }
-- Python: list(filter(lambda x: x > 2, [1, 2, 3, 4, 5]))
-- Lua:
local big = functional.filter({ 1, 2, 3, 4, 5 }, function(x) return x > 2 end)
-- { 3, 4, 5 }
-- Python: functools.reduce(lambda acc, x: acc + x, [1, 2, 3, 4, 5])
-- Lua:
local total = functional.reduce({ 1, 2, 3, 4, 5 }, function(acc, x) return acc + x end)
-- 15
The difference from Python’s map and filter is that these return plain tables immediately, not lazy iterators. For the sizes involved in Excel functions that is almost always what you want.
partial and currying
partial binds the leading arguments of a function, returning a new function that takes the rest. Useful for specialising a general function without rewriting it.
local function multiply(x, y) return x * y end
local double = functional.partial(multiply, 2)
local tenx = functional.partial(multiply, 10)
functional.map({ 1, 2, 3, 4, 5 }, double) -- { 2, 4, 6, 8, 10 }
functional.map({ 1, 2, 3, 4, 5 }, tenx) -- { 10, 20, 30, 40, 50 }
Note that functional.mul(2) does the same thing as functional.partial(multiply, 2) for the common case. The combinators are there so you rarely need partial for basic arithmetic.
A real-world example: weighted scoring
Say you have a range of scores and a range of weights and you need the weighted average.
--- Weighted average of scores given matching weights
-- @param scores number Score values (matrix)
-- @param weights number Weight values (matrix)
function WeightedAverage(scores, weights)
local flat_scores = xllify.flatten_range(scores)
local flat_weights = xllify.flatten_range(weights)
local function mul(a, b) return a * b end
local function add(a, b) return a + b end
local weighted = functional.zip(flat_scores, flat_weights, mul)
local total_weight = functional.reduce(flat_weights, add)
local weighted_sum = functional.reduce(weighted, add)
return weighted_sum / total_weight
end
Without functional this is about twice as much code and you have to mentally track two parallel loop indices.
Predicates: every, some, none
Three short-circuit checks that answer yes/no questions about a table:
local numbers = { 2, 4, 6, 8 }
local function isEven(x) return x % 2 == 0 end
functional.every(numbers, isEven) -- true
functional.some(numbers, function(x) return x > 5 end) -- true
functional.none(numbers, function(x) return x < 0 end) -- true
Useful for validation: check that every cell in a range is numeric, or that at least one value exceeds a threshold, without writing a loop.
Grouping and counting
group_by splits a table into sub-tables keyed by whatever your function returns:
local data = { "apple", "avocado", "banana", "blueberry", "cherry" }
local byLetter = functional.group_by(data, function(s) return s:sub(1, 1) end)
-- { a = {"apple", "avocado"}, b = {"banana", "blueberry"}, c = {"cherry"} }
frequencies counts how many times each distinct value appears:
functional.frequencies({ "yes", "no", "yes", "yes", "no" })
-- { yes = 3, no = 2 }
Slicing: take_while and drop_while
These walk from the front and stop as soon as the predicate fails:
local data = { 1, 3, 5, 6, 8, 9 }
functional.take_while(data, function(x) return x % 2 ~= 0 end) -- { 1, 3, 5 }
functional.drop_while(data, function(x) return x % 2 ~= 0 end) -- { 6, 8, 9 }
distinct removes duplicates, preserving first-seen order:
functional.distinct({ 1, 2, 2, 3, 1, 4 }) -- { 1, 2, 3, 4 }
Combining with xllify.criteria_pred
xllify.criteria_pred turns an Excel-style criteria string into a predicate function you can pass directly to functional.filter (or every, some, none):
local flat = xllify.flatten_range(values)
-- sum values greater than 100
local big = functional.filter(flat, xllify.criteria_pred(">100"))
local total = functional.fold(big, function(acc, v) return acc + v end, 0)
-- count odd values
local odds = functional.filter(flat, xllify.criteria_pred("odd"))
return #odds
This pairs well with functional.fold and functional.reduce to replicate COUNTIF/SUMIF logic in pure Lua with readable intent.
Available everywhere, no setup needed
All of these are built into every Lua state xllify runs. Nothing to import, nothing to add to your build. They are there to cut down on boilerplate and keep your function code focused on what it is computing rather than how it is iterating.
xllify Assistant is aware of the full functional namespace and will use it when generating code where it genuinely makes the result cleaner. It will not shoehorn a pipeline into logic that is more readable as a plain loop.
See the builtins reference for a quick reference of every function in the namespace.