Lua - Functions

Overview

Estimated time: 35–40 minutes

Functions are reusable blocks of code that perform specific tasks. Lua treats functions as first-class values, meaning they can be stored in variables, passed as arguments, and returned from other functions. This tutorial covers all aspects of Lua functions.

Learning Objectives

  • Create and call functions with parameters and return values
  • Understand variable arguments and multiple return values
  • Master local functions and function scope
  • Explore closures and higher-order functions
  • Apply advanced function patterns and best practices

Prerequisites

  • Understanding of Lua variables and scope
  • Knowledge of control flow (if statements, loops)
  • Basic understanding of tables

Basic Function Definition

There are multiple ways to define functions in Lua:

-- Method 1: function statement
function greet()
    print("Hello, World!")
end

-- Method 2: function expression
local say_goodbye = function()
    print("Goodbye!")
end

-- Method 3: local function (preferred for local functions)
local function welcome()
    print("Welcome to Lua!")
end

-- Call the functions
greet()
say_goodbye()
welcome()

Expected Output:

Hello, World!
Goodbye!
Welcome to Lua!

Functions with Parameters

Functions can accept parameters to make them more flexible:

-- Single parameter
local function greet_person(name)
    print("Hello, " .. name .. "!")
end

-- Multiple parameters
local function add_numbers(a, b)
    local result = a + b
    print(a .. " + " .. b .. " = " .. result)
    return result
end

-- Parameters with default values (using or operator)
local function greet_with_title(name, title)
    title = title or "Mr./Ms."  -- Default value
    print("Hello, " .. title .. " " .. name)
end

-- Test the functions
greet_person("Alice")
local sum = add_numbers(5, 3)
print("Returned sum:", sum)

greet_with_title("Bob")
greet_with_title("Carol", "Dr.")

Expected Output:

Hello, Alice!
5 + 3 = 8
Returned sum:	8
Hello, Mr./Ms. Bob
Hello, Dr. Carol

Return Values

Functions can return single or multiple values:

Single Return Value

local function square(x)
    return x * x
end

local function is_even(num)
    return num % 2 == 0
end

local function get_grade(score)
    if score >= 90 then
        return "A"
    elseif score >= 80 then
        return "B"
    elseif score >= 70 then
        return "C"
    else
        return "F"
    end
end

print("Square of 7:", square(7))
print("Is 10 even?", is_even(10))
print("Grade for 85:", get_grade(85))

Multiple Return Values

-- Function returning multiple values
local function divide_with_remainder(dividend, divisor)
    local quotient = dividend // divisor
    local remainder = dividend % divisor
    return quotient, remainder
end

local function get_name_parts(full_name)
    local space_pos = string.find(full_name, " ")
    if space_pos then
        local first = string.sub(full_name, 1, space_pos - 1)
        local last = string.sub(full_name, space_pos + 1)
        return first, last
    else
        return full_name, ""
    end
end

-- Using multiple return values
local q, r = divide_with_remainder(17, 5)
print("17 ÷ 5 = " .. q .. " remainder " .. r)

local first_name, last_name = get_name_parts("John Doe")
print("First:", first_name, "Last:", last_name)

-- Ignoring some return values
local quotient = divide_with_remainder(20, 3)  -- Only gets first return value
print("Quotient only:", quotient)

Expected Output:

Square of 7:	49
Is 10 even?	true
Grade for 85:	B
17 ÷ 5 = 3 remainder 2
First:	John	Last:	Doe
Quotient only:	6

Variable Arguments

Functions can accept variable number of arguments using ...:

-- Function with variable arguments
local function sum_all(...)
    local args = {...}  -- Pack arguments into table
    local total = 0
    
    for i, value in ipairs(args) do
        total = total + value
    end
    
    return total
end

-- Function to print all arguments
local function print_all(...)
    local args = {...}
    print("Number of arguments:", #args)
    
    for i, arg in ipairs(args) do
        print("Arg " .. i .. ":", arg)
    end
end

-- Function combining fixed and variable arguments
local function greet_multiple(greeting, ...)
    local names = {...}
    
    for i, name in ipairs(names) do
        print(greeting .. ", " .. name .. "!")
    end
end

-- Test variable argument functions
print("Sum of 1,2,3,4,5:", sum_all(1, 2, 3, 4, 5))
print("Sum of 10,20:", sum_all(10, 20))

print_all("apple", "banana", "orange")

greet_multiple("Hello", "Alice", "Bob", "Charlie")

Expected Output:

Sum of 1,2,3,4,5:	15
Sum of 10,20:	30
Number of arguments:	3
Arg 1:	apple
Arg 2:	banana
Arg 3:	orange
Hello, Alice!
Hello, Bob!
Hello, Charlie!

Local vs Global Functions

Always prefer local functions for better performance and encapsulation:

-- Global function (avoid unless necessary)
function global_function()
    return "I'm global"
end

-- Local function (preferred)
local function local_function()
    return "I'm local"
end

-- Functions defined inside other functions
local function outer_function()
    local function inner_function()
        return "I'm inside outer_function"
    end
    
    print("Outer function called")
    print("Inner function says:", inner_function())
end

print(global_function())
print(local_function())
outer_function()

-- inner_function is not accessible here
-- print(inner_function())  -- This would cause an error

Expected Output:

I'm global
I'm local
Outer function called
Inner function says:	I'm inside outer_function

Functions as First-Class Values

Functions can be stored in variables, tables, and passed as arguments:

-- Storing functions in variables
local function add(a, b)
    return a + b
end

local function multiply(a, b)
    return a * b
end

-- Store functions in a table
local math_operations = {
    add = add,
    multiply = multiply,
    subtract = function(a, b) return a - b end,
    divide = function(a, b) 
        return b ~= 0 and a / b or "Division by zero"
    end
}

-- Using functions from table
print("5 + 3 =", math_operations.add(5, 3))
print("5 * 3 =", math_operations.multiply(5, 3))
print("5 - 3 =", math_operations.subtract(5, 3))
print("5 / 3 =", math_operations.divide(5, 3))

-- Passing functions as arguments
local function apply_operation(func, x, y)
    return func(x, y)
end

print("Using apply_operation:")
print("Apply add:", apply_operation(add, 10, 5))
print("Apply multiply:", apply_operation(multiply, 10, 5))

Expected Output:

5 + 3 =	8
5 * 3 =	15
5 - 3 =	2
5 / 3 =	1.6666666666667
Using apply_operation:
Apply add:	15
Apply multiply:	50

Closures

Closures allow functions to access variables from their enclosing scope:

-- Basic closure
local function create_counter()
    local count = 0
    
    return function()
        count = count + 1
        return count
    end
end

local counter1 = create_counter()
local counter2 = create_counter()

print("Counter1:", counter1())  -- 1
print("Counter1:", counter1())  -- 2
print("Counter2:", counter2())  -- 1 (independent counter)
print("Counter1:", counter1())  -- 3

-- Closure with parameters
local function create_multiplier(factor)
    return function(number)
        return number * factor
    end
end

local double = create_multiplier(2)
local triple = create_multiplier(3)

print("Double 5:", double(5))
print("Triple 4:", triple(4))

-- More complex closure example
local function create_bank_account(initial_balance)
    local balance = initial_balance or 0
    
    return {
        deposit = function(amount)
            if amount > 0 then
                balance = balance + amount
                return "Deposited " .. amount .. ". New balance: " .. balance
            else
                return "Invalid deposit amount"
            end
        end,
        
        withdraw = function(amount)
            if amount > 0 and amount <= balance then
                balance = balance - amount
                return "Withdrew " .. amount .. ". New balance: " .. balance
            else
                return "Invalid withdrawal amount or insufficient funds"
            end
        end,
        
        get_balance = function()
            return balance
        end
    }
end

local account = create_bank_account(100)
print(account.deposit(50))
print(account.withdraw(30))
print("Current balance:", account.get_balance())

Expected Output:

Counter1:	1
Counter1:	2
Counter2:	1
Counter1:	3
Double 5:	10
Triple 4:	12
Deposited 50. New balance: 150
Withdrew 30. New balance: 120
Current balance:	120

Higher-Order Functions

Functions that take other functions as arguments or return functions:

-- Map function (applies function to each element)
local function map(func, list)
    local result = {}
    
    for i, value in ipairs(list) do
        result[i] = func(value)
    end
    
    return result
end

-- Filter function (keeps elements that pass test)
local function filter(predicate, list)
    local result = {}
    
    for i, value in ipairs(list) do
        if predicate(value) then
            table.insert(result, value)
        end
    end
    
    return result
end

-- Reduce function (combines all elements)
local function reduce(func, list, initial)
    local accumulator = initial
    
    for i, value in ipairs(list) do
        accumulator = func(accumulator, value)
    end
    
    return accumulator
end

-- Test higher-order functions
local numbers = {1, 2, 3, 4, 5}

local squared = map(function(x) return x * x end, numbers)
print("Squared:", table.concat(squared, ", "))

local evens = filter(function(x) return x % 2 == 0 end, numbers)
print("Even numbers:", table.concat(evens, ", "))

local sum = reduce(function(acc, x) return acc + x end, numbers, 0)
print("Sum:", sum)

local product = reduce(function(acc, x) return acc * x end, numbers, 1)
print("Product:", product)

Expected Output:

Squared:	1, 4, 9, 16, 25
Even numbers:	2, 4
Sum:	15
Product:	120

Recursive Functions

Functions that call themselves:

-- Factorial using recursion
local function factorial(n)
    if n <= 1 then
        return 1
    else
        return n * factorial(n - 1)
    end
end

-- Fibonacci sequence
local function fibonacci(n)
    if n <= 2 then
        return 1
    else
        return fibonacci(n - 1) + fibonacci(n - 2)
    end
end

-- Binary search (recursive)
local function binary_search(arr, target, left, right)
    left = left or 1
    right = right or #arr
    
    if left > right then
        return nil  -- Not found
    end
    
    local mid = math.floor((left + right) / 2)
    
    if arr[mid] == target then
        return mid
    elseif arr[mid] < target then
        return binary_search(arr, target, mid + 1, right)
    else
        return binary_search(arr, target, left, mid - 1)
    end
end

-- Test recursive functions
print("5! =", factorial(5))
print("6th Fibonacci number:", fibonacci(6))

local sorted_array = {1, 3, 5, 7, 9, 11, 13, 15}
local position = binary_search(sorted_array, 7)
print("Found 7 at position:", position)

Expected Output:

5! =	120
6th Fibonacci number:	8
Found 7 at position:	4

Practical Function Examples

Utility Functions Library

-- String utilities
local string_utils = {
    trim = function(str)
        return string.match(str, "^%s*(.-)%s*$")
    end,
    
    split = function(str, delimiter)
        local result = {}
        local pattern = "(.-)" .. delimiter
        local last_end = 1
        
        for part in string.gfind(str .. delimiter, pattern) do
            table.insert(result, part)
        end
        
        return result
    end,
    
    capitalize = function(str)
        return string.upper(string.sub(str, 1, 1)) .. string.lower(string.sub(str, 2))
    end
}

-- Table utilities
local table_utils = {
    deep_copy = function(original)
        local copy
        if type(original) == 'table' then
            copy = {}
            for key, value in pairs(original) do
                copy[key] = table_utils.deep_copy(value)
            end
        else
            copy = original
        end
        return copy
    end,
    
    merge = function(t1, t2)
        local result = {}
        
        for k, v in pairs(t1) do
            result[k] = v
        end
        
        for k, v in pairs(t2) do
            result[k] = v
        end
        
        return result
    end,
    
    keys = function(t)
        local keys = {}
        for k, v in pairs(t) do
            table.insert(keys, k)
        end
        return keys
    end
}

-- Test utility functions
print("Trimmed:", "'" .. string_utils.trim("  hello world  ") .. "'")
print("Capitalized:", string_utils.capitalize("lua programming"))

local original = {a = 1, b = {c = 2}}
local copy = table_utils.deep_copy(original)
copy.b.c = 3
print("Original b.c:", original.b.c)  -- Still 2
print("Copy b.c:", copy.b.c)          -- Now 3

local merged = table_utils.merge({a = 1, b = 2}, {b = 3, c = 4})
local keys = table_utils.keys(merged)
print("Merged keys:", table.concat(keys, ", "))

Expected Output:

Trimmed:	'hello world'
Capitalized:	Lua programming
Original b.c:	2
Copy b.c:	3
Merged keys:	a, b, c

Common Pitfalls

  • Global functions: Always use local function unless you need global access
  • Missing return: Functions without explicit return statements return nil
  • Variable scope: Variables inside functions are local to that function
  • Stack overflow: Be careful with deep recursion
  • Closure gotchas: Closures capture variables by reference, not value

Checks for Understanding

  1. What are the three ways to define a function in Lua?
  2. How do you return multiple values from a function?
  3. What does ... represent in function parameters?
  4. What is a closure and how is it useful?
  5. How do you pass a function as an argument to another function?
Show answers
  1. Function statement (function name()), function expression (local f = function()), and local function (local function name())
  2. Use multiple values in the return statement: return value1, value2, value3
  3. Variable arguments - allows a function to accept any number of arguments
  4. A closure is a function that captures variables from its enclosing scope. Useful for creating specialized functions and maintaining private state.
  5. Just pass the function name as an argument: my_function(other_function, args)

Next Steps

Functions are fundamental to Lua programming. Now that you've mastered functions, you're ready to explore arrays and advanced table manipulation techniques.