Functions
Functions in Lua are first-class values. Pass them around, return them, store them in tables.
Function Basics
Definition
-- Basic function
local function greet(name)
return "Hello, " .. name
end
-- Anonymous function (assigned to variable)
local greet = function(name)
return "Hello, " .. name
end
-- Global function (avoid)
function bad_global_function()
return "Don't do this"
end
-- Call
print(greet("Evan")) -- "Hello, Evan"
Parameters
-- Multiple parameters
local function connect(host, port, timeout)
print(string.format("Connecting to %s:%d (timeout=%d)", host, port, timeout))
end
connect("ise-01", 443, 30)
-- Unused parameters (use _)
local function callback(_, value)
print(value)
end
-- All parameters are optional!
connect("ise-01") -- port and timeout are nil
-- Default values (manual)
local function connect(host, port, timeout)
port = port or 443
timeout = timeout or 30
print(string.format("Connecting to %s:%d (timeout=%d)", host, port, timeout))
end
Return Values
-- Single return
local function get_host()
return "ise-01"
end
-- Multiple returns
local function get_host_info()
return "ise-01", "10.50.1.20", 443
end
local host, ip, port = get_host_info()
-- Return early
local function validate_ip(ip)
if not ip then
return false, "IP is required"
end
-- ... validation
return true
end
local valid, err = validate_ip(nil)
if not valid then
print("Error: " .. err)
end
Multiple Returns Gotchas
local function get_values()
return 1, 2, 3
end
-- All values captured
local a, b, c = get_values() -- a=1, b=2, c=3
-- Extra variables get nil
local a, b, c, d = get_values() -- d=nil
-- In expression, only first value used
print(get_values()) -- 1 2 3 (all printed)
print(get_values() .. "!") -- "1!" (only first)
-- Force single value with parentheses
local a = (get_values()) -- a=1 only
Variadic Functions
-- Accept any number of arguments with ...
local function log(level, ...)
local args = {...}
print(string.format("[%s] %s", level, table.concat(args, " ")))
end
log("INFO", "Connected", "to", "ise-01")
-- [INFO] Connected to ise-01
-- Get number of variadic args
local function count_args(...)
return select("#", ...)
end
print(count_args(1, 2, 3)) -- 3
-- Access specific variadic arg
local function get_third(...)
return select(3, ...)
end
print(get_third("a", "b", "c", "d")) -- "c" "d" (from 3rd onward)
-- Pack/unpack
local function process(...)
local args = {...}
-- or: local args = table.pack(...)
for i, arg in ipairs(args) do
print(i, arg)
end
end
local values = {1, 2, 3}
process(table.unpack(values)) -- Same as process(1, 2, 3)
First-Class Functions
Functions as Values
-- Store in variable
local fn = function(x) return x * 2 end
-- Store in table
local operations = {
double = function(x) return x * 2 end,
triple = function(x) return x * 3 end,
}
print(operations.double(5)) -- 10
-- Pass as argument
local function apply(fn, value)
return fn(value)
end
print(apply(operations.double, 5)) -- 10
-- Return from function
local function make_multiplier(n)
return function(x)
return x * n
end
end
local double = make_multiplier(2)
local triple = make_multiplier(3)
print(double(5)) -- 10
print(triple(5)) -- 15
Anonymous Functions
-- Inline callbacks
table.sort(hosts, function(a, b)
return a < b
end)
-- Event handlers
vim.keymap.set("n", "<leader>f", function()
print("Key pressed!")
end)
-- Filter with anonymous function
local active = vim.tbl_filter(function(ep)
return ep.status == "active"
end, endpoints)
Closures
-- Function captures variables from enclosing scope
local function make_counter()
local count = 0
return function()
count = count + 1
return count
end
end
local counter = make_counter()
print(counter()) -- 1
print(counter()) -- 2
print(counter()) -- 3
-- Practical: Rate limiter
local function make_rate_limiter(max_calls, window_seconds)
local calls = {}
return function()
local now = os.time()
-- Remove old calls
local valid_calls = {}
for _, t in ipairs(calls) do
if now - t < window_seconds then
table.insert(valid_calls, t)
end
end
calls = valid_calls
if #calls < max_calls then
table.insert(calls, now)
return true
end
return false
end
end
local limiter = make_rate_limiter(5, 10) -- 5 calls per 10 seconds
-- Practical: Logger with prefix
local function make_logger(prefix)
return function(msg)
print(string.format("[%s] %s", prefix, msg))
end
end
local info = make_logger("INFO")
local error = make_logger("ERROR")
info("Connected") -- [INFO] Connected
error("Failed") -- [ERROR] Failed
Higher-Order Functions
-- Functions that take or return functions
-- Map
local function map(t, fn)
local result = {}
for i, v in ipairs(t) do
result[i] = fn(v)
end
return result
end
local doubled = map({1, 2, 3}, function(x) return x * 2 end)
-- Filter
local function filter(t, predicate)
local result = {}
for _, v in ipairs(t) do
if predicate(v) then
table.insert(result, v)
end
end
return result
end
local even = filter({1, 2, 3, 4}, function(x) return x % 2 == 0 end)
-- Reduce
local function reduce(t, fn, initial)
local acc = initial
for _, v in ipairs(t) do
acc = fn(acc, v)
end
return acc
end
local sum = reduce({1, 2, 3, 4}, function(acc, x) return acc + x end, 0)
-- Compose
local function compose(f, g)
return function(x)
return f(g(x))
end
end
local double = function(x) return x * 2 end
local add_one = function(x) return x + 1 end
local double_then_add = compose(add_one, double)
print(double_then_add(5)) -- 11
Method Syntax
-- Colon syntax passes self automatically
local Host = {}
function Host.new(hostname, ip)
return {
hostname = hostname,
ip = ip
}
end
-- Regular function call
function Host.get_fqdn(self)
return self.hostname .. ".inside.domusdigitalis.dev"
end
-- Method syntax (syntactic sugar)
function Host:get_url()
return "https://" .. self:get_fqdn() .. ":443"
end
local host = Host.new("ise-01", "10.50.1.20")
-- These are equivalent:
print(Host.get_fqdn(host))
print(host:get_fqdn()) -- Colon passes host as self
Function Patterns
Options Table
-- Instead of many parameters, use options table
local function connect(opts)
opts = opts or {}
local host = opts.host or "localhost"
local port = opts.port or 443
local timeout = opts.timeout or 30
local verify = opts.verify_ssl ~= false -- Default true
print(string.format("Connecting to %s:%d", host, port))
end
connect({
host = "ise-01",
port = 9060,
verify_ssl = false
})
-- With vim.tbl_deep_extend
local function connect(opts)
local defaults = {
host = "localhost",
port = 443,
timeout = 30,
verify_ssl = true
}
opts = vim.tbl_deep_extend("force", defaults, opts or {})
-- ...
end
Builder Pattern
local function RequestBuilder()
local request = {
method = "GET",
headers = {}
}
local builder = {}
function builder.method(m)
request.method = m
return builder
end
function builder.url(u)
request.url = u
return builder
end
function builder.header(key, value)
request.headers[key] = value
return builder
end
function builder.build()
return request
end
return builder
end
local req = RequestBuilder()
.method("POST")
.url("/api/endpoint")
.header("Content-Type", "application/json")
.build()
Memoization
local function memoize(fn)
local cache = {}
return function(...)
local key = table.concat({...}, ",")
if cache[key] == nil then
cache[key] = fn(...)
end
return cache[key]
end
end
local function expensive_lookup(id)
print("Looking up " .. id)
return "result-" .. id
end
local cached_lookup = memoize(expensive_lookup)
print(cached_lookup("123")) -- "Looking up 123" then "result-123"
print(cached_lookup("123")) -- "result-123" (cached, no print)
Neovim Function Patterns
-- Keymap callback
vim.keymap.set("n", "<leader>e", function()
vim.cmd("Explore")
end, { desc = "Open file explorer" })
-- Autocommand callback
vim.api.nvim_create_autocmd("BufWritePre", {
callback = function(args)
-- args.buf, args.file, args.match available
vim.lsp.buf.format()
end,
})
-- Plugin setup function
local M = {}
function M.setup(opts)
opts = vim.tbl_deep_extend("force", {
enabled = true,
key = "<leader>x",
}, opts or {})
if opts.enabled then
vim.keymap.set("n", opts.key, M.do_action)
end
end
function M.do_action()
print("Action!")
end
return M
Next Module
Metatables - Operator overloading and OOP patterns.