xllify

Functional extensions for Lua

Higher-order functional primitives now built into xllify

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 than t.map(f): the functions are not methods on the table
  • function(x) return x * 2 end rather than x => 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.

← All posts