Web Analytics

Closures in Lua

Intermediate ~30 min read

A closure is a function that captures and remembers variables from its surrounding scope, even after that scope has finished executing. Closures are one of Lua's most powerful features, enabling elegant solutions to complex problems. They're essential for creating private variables, factory functions, callbacks, and functional programming patterns. Let's explore how closures work and how to use them effectively!

What is a Closure?

A closure is created when a function accesses variables from an outer scope:

local function createCounter()
    local count = 0  -- This variable is "captured"
    
    return function()
        count = count + 1
        return count
    end
end

local counter = createCounter()
print(counter())  -- 1
print(counter())  -- 2
print(counter())  -- 3
Key Concept: The inner function "closes over" the count variable. Even though createCounter() has finished executing, the returned function still has access to count!
Output
Click Run to execute your code

Lexical Scoping

Lua uses lexical scoping, meaning functions can access variables from their enclosing scopes:

local x = 10

local function outer()
    local y = 20
    
    local function inner()
        local z = 30
        print(x, y, z)  -- Can access all three!
    end
    
    inner()
end

outer()  -- 10  20  30
Tip: Variables captured by closures are called upvalues. Each closure maintains its own copy of upvalues, allowing multiple independent closures.

Private Variables

Closures enable true private variables in Lua:

local function createBankAccount(initialBalance)
    local balance = initialBalance  -- Private!
    
    return {
        deposit = function(amount)
            balance = balance + amount
            return balance
        end,
        
        withdraw = function(amount)
            if amount > balance then
                return nil, "Insufficient funds"
            end
            balance = balance - amount
            return balance
        end,
        
        getBalance = function()
            return balance
        end
    }
end

local account = createBankAccount(1000)
print(account.getBalance())  -- 1000
account.deposit(500)
print(account.getBalance())  -- 1500
-- print(balance)  -- Error: balance is not accessible!
Output
Click Run to execute your code

Factory Functions

Closures are perfect for creating factory functions that generate customized functions:

local function createMultiplier(factor)
    return function(x)
        return x * factor
    end
end

local double = createMultiplier(2)
local triple = createMultiplier(3)
local quadruple = createMultiplier(4)

print(double(5))     -- 10
print(triple(5))     -- 15
print(quadruple(5))  -- 20

More Factory Examples

-- Greeting factory
local function createGreeter(greeting)
    return function(name)
        return greeting .. ", " .. name .. "!"
    end
end

local sayHello = createGreeter("Hello")
local sayHi = createGreeter("Hi")

print(sayHello("Alice"))  -- Hello, Alice!
print(sayHi("Bob"))       -- Hi, Bob!

-- Validator factory
local function createValidator(min, max)
    return function(value)
        return value >= min and value <= max
    end
end

local isValidAge = createValidator(0, 120)
local isValidScore = createValidator(0, 100)

print(isValidAge(25))   -- true
print(isValidScore(150)) -- false
Output
Click Run to execute your code

Callbacks and Event Handlers

Closures are commonly used for callbacks that need to remember context:

local function createButton(label)
    local clickCount = 0
    
    return {
        onClick = function()
            clickCount = clickCount + 1
            print(label .. " clicked " .. clickCount .. " times")
        end
    }
end

local button1 = createButton("Submit")
local button2 = createButton("Cancel")

button1.onClick()  -- Submit clicked 1 times
button1.onClick()  -- Submit clicked 2 times
button2.onClick()  -- Cancel clicked 1 times

Custom Iterators

Closures enable powerful custom iterators:

local function range(from, to, step)
    step = step or 1
    local current = from - step
    
    return function()
        current = current + step
        if current <= to then
            return current
        end
    end
end

-- Use in for loop
for i in range(1, 10, 2) do
    print(i)  -- 1, 3, 5, 7, 9
end

-- Fibonacci iterator
local function fibonacci()
    local a, b = 0, 1
    return function()
        a, b = b, a + b
        return a
    end
end

local fib = fibonacci()
for i = 1, 10 do
    print(fib())  -- 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
end
Output
Click Run to execute your code

Memoization Pattern

Use closures to cache expensive function results:

local function memoize(func)
    local cache = {}
    
    return function(x)
        if cache[x] == nil then
            cache[x] = func(x)
        end
        return cache[x]
    end
end

-- Expensive fibonacci
local function fib(n)
    if n <= 1 then return n end
    return fib(n - 1) + fib(n - 2)
end

local fastFib = memoize(fib)
print(fastFib(30))  -- Fast!
print(fastFib(30))  -- Even faster (cached)!
Best Practice: Memoization is a powerful optimization technique. Use it for pure functions (same input always gives same output) with expensive computations.

Common Closure Patterns

1. Configuration Object

local function createConfig()
    local settings = {}
    
    return {
        set = function(key, value)
            settings[key] = value
        end,
        
        get = function(key)
            return settings[key]
        end,
        
        getAll = function()
            local copy = {}
            for k, v in pairs(settings) do
                copy[k] = v
            end
            return copy
        end
    }
end

2. State Machine

local function createStateMachine()
    local state = "idle"
    
    return {
        getState = function()
            return state
        end,
        
        transition = function(newState)
            print("Transitioning from " .. state .. " to " .. newState)
            state = newState
        end
    }
end

3. Partial Application

local function partial(func, ...)
    local args = {...}
    return function(...)
        local allArgs = {}
        for i, v in ipairs(args) do
            table.insert(allArgs, v)
        end
        for i, v in ipairs({...}) do
            table.insert(allArgs, v)
        end
        return func(table.unpack(allArgs))
    end
end

local function add(a, b, c)
    return a + b + c
end

local add5 = partial(add, 5)
print(add5(3, 2))  -- 10
Output
Click Run to execute your code

Practice Exercise

Try these closure challenges:

Output
Click Run to execute your code

Summary

In this lesson, you learned:

  • What closures are and how they work
  • Lexical scoping and upvalues
  • Creating private variables with closures
  • Factory functions for generating customized functions
  • Using closures for callbacks and event handlers
  • Custom iterators with closures
  • Memoization pattern for optimization
  • Common closure patterns (config, state machine, partial application)

What's Next?

You've mastered closuresβ€”one of Lua's most powerful features! Next, we'll explore advanced function techniques including higher-order functions, function composition, currying, and functional programming patterns. Let's continue! πŸš€