Part of Learn Lua with NeoVim

Learn Lua in Neovim: First-Class Functions — Store, Pass & Filter with Functions | Episode 15

Sandy LaneSandy Lane

Video: Learn Lua in Neovim: First-Class Functions — Store, Pass & Filter with Functions | Episode 15 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Lua First-Class Functions: Store, Pass, and Filter

Functions are values. local fn = function(x) ... end stores one in a variable. Pass them as arguments to build callbacks. Build a filter function 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.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.