Learn Lua in Neovim: First-Class Functions — Store, Pass & Filter with Functions | Episode 15
Video: Learn Lua in Neovim: First-Class Functions — Store, Pass & Filter with Functions | Episode 15 by Taught by Celeste AI - AI Coding Coach
Lua First-Class Functions: Store, Pass, and Filter
Functions are values.
local fn = function(x) ... endstores one in a variable. Pass them as arguments to build callbacks. Build afilterfunction that selects items matching a test.
In Lua, functions are first-class values: you can store them in variables, pass them to other functions, return them from functions. This unlocks the patterns the next few episodes are built on — closures, callbacks, and OOP.
Functions in variables
local square = function(x)
return x * x
end
print("5 squared is " .. square(5))
print("9 squared is " .. square(9))
Output:
5 squared is 25
9 squared is 81
The right-hand side function(x) return x * x end is an anonymous function — a function with no name. We assign it to square, then call square(...) like any normal function.
Equivalent to:
local function square(x)
return x * x
end
The local function name(...) ... end form is sugar for local name; name = function(...) ... end. Both produce the same value.
Functions are values
local f = print -- f is now a reference to print
f("Hello") -- works, prints "Hello"
print itself is a value — assigning it to f gives f the same function. There's no "function declaration" vs "function reference" distinction; functions just are values.
Callbacks: passing a function as an argument
function apply(fn, x)
return fn(x)
end
print(apply(math.sqrt, 25)) -- 5.0
print(apply(square, 7)) -- 49
apply takes a function and a value, calls the function on the value. The function is just a regular argument.
math.sqrt is a function value (Lua's standard library). We pass it to apply, which calls fn(x) — same as if you'd written math.sqrt(25).
Anonymous functions inline
You can pass a freshly-created function without naming it:
print(apply(function(x) return x + 100 end, 5))
-- 105
Reads as: "apply (a function that takes x and returns x + 100) to 5."
Useful when the function is one-shot — no point naming it.
Building filter
function filter(items, test)
local result = {}
for _, item in ipairs(items) do
if test(item) then
table.insert(result, item)
end
end
return result
end
filter(items, test) walks the list, calls test(item) on each, and collects items where the test returns truthy.
test is a function — the caller decides what "matching" means.
Using filter
local numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
-- only even numbers
local evens = filter(numbers, function(n) return n % 2 == 0 end)
for _, v in ipairs(evens) do print(v) end
-- 2, 4, 6, 8, 10
-- only > 5
local big = filter(numbers, function(n) return n > 5 end)
for _, v in ipairs(big) do print(v) end
-- 6, 7, 8, 9, 10
Same filter function, different tests. The pattern (often called a higher-order function) generalises:
function map(items, fn)
local result = {}
for i, v in ipairs(items) do
result[i] = fn(v)
end
return result
end
local squared = map({1, 2, 3, 4}, function(n) return n * n end)
-- {1, 4, 9, 16}
map is filter's sibling — apply a function to each item.
function reduce(items, fn, init)
local acc = init
for _, v in ipairs(items) do
acc = fn(acc, v)
end
return acc
end
local sum = reduce({1, 2, 3, 4, 5}, function(a, b) return a + b end, 0)
-- 15
reduce (or fold) collapses a list to one value via repeated application.
map, filter, reduce are the three core higher-order functions — same trio you'd see in functional languages, JavaScript, Python's functools, etc.
Returning a function
function make_greeter(greeting)
return function(name)
return greeting .. ", " .. name .. "!"
end
end
local hi = make_greeter("Hi")
local hola = make_greeter("Hola")
print(hi("Alice")) -- "Hi, Alice!"
print(hola("Bob")) -- "Hola, Bob!"
make_greeter returns a new function each time it's called. The returned function "remembers" the greeting value from when it was made. That's a closure — the topic of the next episode.
Functions in tables
local ops = {
add = function(a, b) return a + b end,
sub = function(a, b) return a - b end,
mul = function(a, b) return a * b end,
div = function(a, b) return a / b end,
}
print(ops.add(2, 3)) -- 5
print(ops["mul"](4, 5)) -- 20
Functions stored as table values is how Lua does most "objects" and "modules." Table indexing works for retrieval; the result is callable.
ops.add and ops["add"] both retrieve the same value. Dot syntax for known field names; bracket syntax for dynamic ones.
Comparing functions
local f = function() return 1 end
local g = function() return 1 end
print(f == g) -- false
print(f == f) -- true
Function equality is reference equality. Two functions with identical bodies are not equal — only the same function value compares equal to itself.
Common stumbles
Calling vs referencing. apply(math.sqrt, 25) passes the function. apply(math.sqrt(25), 25) passes the result of calling math.sqrt(25) — usually a bug. The first version is what you want.
Capturing the wrong variable. Returned functions close over the variable, not the value at the time of definition. We dive into this in episode 16.
Forgetting function ... end. Anonymous function syntax has no shorthand in Lua (no => or lambda). It's always function(args) ... end.
Passing too many or too few args to a callback. Lua won't error — extras get dropped, missing become nil. If your callback errors mid-way, double-check argument counts.
Equality on function values. Reference-only. To "compare" two functions, you'd have to compare the bytecode, which Lua doesn't expose.
What's next
Episode 16: closures. Functions that remember their environment. We'll look at exactly how the returned-function pattern from make_greeter retains the greeting value, and use it to build a counter factory.
Recap
Functions are first-class values: store in variables, pass as arguments, return from other functions. Anonymous: function(args) ... end. The trio of higher-order functions: map, filter, reduce. Functions in tables = "methods" (groundwork for OOP). Function equality is reference-only.
Next episode: closures.