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.