Skip to main content
    Courses/Lua/Functions and Tables

    Lesson 4 • Beginner

    Functions

    By the end of this lesson you'll define and call functions, return several values at once, accept any number of arguments with ..., give arguments sensible defaults, pass functions around as values, capture state in closures, and solve nested problems with recursion — the toolkit behind every real Lua script.

    What You'll Learn

    • Define and call functions, preferring local function for scope
    • Return multiple values with return a, b — and catch them
    • Accept any number of arguments using varargs (... and select)
    • Give arguments defaults with the x = x or fallback idiom
    • Treat functions as first-class values: store, pass, and return them
    • Build closures that remember state, and solve problems with recursion

    💡 Real-World Analogy

    A function is a recipe card. The ingredients you hand it are the arguments; the finished dish it gives back is the return value. Write the recipe once and you can cook it any night without re-deriving it. Lua's recipes have two superpowers most kitchens lack: one card can plate up several dishes at once (multiple returns), and a recipe can keep a private pantry that remembers what you used last time (a closure). Master the recipe card and you stop repeating yourself.

    1. Defining and Calling Functions

    A function bundles a piece of work under a name so you can run it again whenever you like. The shape is function name(args) ... end, and you put local in front to keep it scoped — the same safe default you learned for variables. Inside, return hands a value back to whoever called it; a function with no return still runs (often for a side effect like printing) and quietly gives back nil. Read this worked example and run it.

    Worked example: define, call, return, and local scope
    -- A function packages up some work so you can run it again and again.
    -- The shape is:  function name(arguments) ... return value end
    -- Put 'local' in front so the function stays scoped (your default!).
    local function greet(name)
      return "Hello, " .. name .. "!"   -- '..' joins text together
    end
    
    -- Call it by writing its name followed by (arguments):
    print(greet("Ada"))     -- prints: Hello, Ada!
    print(greet("Lin"))     -- prints: Hello, Lin!
    
    -- Without 'return' a function just DOES something and hands back nil:
    local function shout(word)
      print(word .. "!!!")  -- this prints as a side effect
    end
    shout("go")             -- prints: go!!!
    local result = shout("hi")  -- prints: hi!!!
    print(result)           -- prints: nil   (shout returned nothing)
    
    -- Leaving off 'local' makes a GLOBAL function — visible everywhere,
    -- which is exactly the namespace pollution you want to avoid.
    Lua runs outside the browser — try this code atonecompiler.com/luaor locally with the lua command.

    2. Multiple Return Values

    Here's something most languages can't do: a Lua function can return several values at once — just separate them with commas after return. You catch them by listing the same number of variables on the left: local q, r = divide(17, 5). There's one rule that trips everyone up: a function call is cut down to a single value unless it's the last thing in a list. Put the multi-return call last and all its values survive; put it earlier and only the first is kept.

    Worked example: return a, b and the 'last position' rule
    -- Lua functions can return MORE THAN ONE value. Just list them after return.
    local function divide(a, b)
      return a // b, a % b      -- // is integer divide, % is remainder
    end
    
    -- Catch each returned value in its own variable, left to right:
    local q, r = divide(17, 5)
    print(q, r)                 -- prints: 3   2
    
    -- A handy real example: split a full name into first and last.
    local function splitName(full)
      return full:match("(%w+)%s+(%w+)")   -- pattern returns TWO captures
    end
    local first, last = splitName("Grace Hopper")
    print(first, last)          -- prints: Grace   Hopper
    
    -- ⚠️ A function call is TRUNCATED to one value unless it's LAST in a list.
    local function pair() return 1, 2 end
    print(pair(), 99)           -- prints: 1   99   (pair() cut to just 1)
    print(99, pair())           -- prints: 99   1   2   (pair() is last -> all kept)
    Lua runs outside the browser — try this code atonecompiler.com/luaor locally with the lua command.

    Your turn. Finish the minMax function so it returns the smaller value first and the larger second, then catch both returned values. Fill in the blanks marked ___ and compare with the expected output.

    🎯 Your turn: return two values and catch them
    -- 🎯 YOUR TURN — replace each ___ then run the code.
    
    -- Write a function that returns BOTH the min and the max of two numbers.
    local function minMax(a, b)
      -- 1) If a is smaller, the order is a, b — otherwise b, a
      if a < b then
        return ___, ___      -- 👉 smaller first, then larger: a, b
      else
        return ___, ___      -- 👉 b, a
      end
    end
    
    -- 2) Catch BOTH returned values in two variables, left to right
    local low, high = ___    -- 👉 minMax(8, 3)
    
    print(low, high)
    
    -- ✅ Expected output:
    -- 3   8
    Lua runs outside the browser — try this code atonecompiler.com/luaor locally with the lua command.

    3. Varargs: Any Number of Arguments

    Sometimes you don't know in advance how many arguments a function will get — think print, which accepts as many as you throw at it. Lua's answer is varargs, written as ... (three dots) in the parameter list. Pack them into a table with {...} so you can loop over them, count them with select("#", ...), or read from a position with select(i, ...). When some arguments might be nil, use table.pack(...) instead — it records an accurate count in .n that {...} can't promise.

    Worked example: ..., select, and table.pack
    -- '...' (three dots) means "any number of extra arguments".
    -- It collects everything the caller passed into a vararg list.
    local function sum(...)
      local total = 0
      for _, n in ipairs({...}) do   -- {...} packs the args into a table
        total = total + n
      end
      return total
    end
    print(sum(1, 2, 3))         -- prints: 6
    print(sum(10, 20, 30, 40))  -- prints: 100
    
    -- select("#", ...) counts how many varargs arrived.
    -- select(i, ...)   returns everything from position i onward.
    local function describe(...)
      print("got " .. select("#", ...) .. " arguments")
      print("first one is " .. select(1, ...))
    end
    describe("a", "b", "c")
    -- prints: got 3 arguments
    -- prints: first one is a
    
    -- table.pack keeps a correct count even when some args are nil.
    local packed = table.pack(1, nil, 3)
    print(packed.n, packed[1], packed[3])   -- prints: 3   1   3
    Lua runs outside the browser — try this code atonecompiler.com/luaor locally with the lua command.

    Now you try. Build an average function that accepts any number of scores using ..., sums them, and divides by how many there were. Fill in the three blanks:

    🎯 Your turn: average any number of values
    -- 🎯 YOUR TURN — build an average() that accepts any number of scores.
    
    -- 1) Use ... so the caller can pass as many numbers as they like
    local function average(...)
      local total = 0
      local nums = {___}            -- 👉 pack the varargs into a table: ...
    
      for _, n in ipairs(nums) do
        total = total + n
      end
    
      -- 2) How many numbers came in? Use the # length operator on the table
      local count = ___             -- 👉 #nums
    
      -- 3) Avoid divide-by-zero: if count is 0, the answer is 0
      if count == 0 then return 0 end
      return total / count
    end
    
    print(average(10, 20, 30))      -- 👉 call it with three numbers
    print(average(4, 8))
    
    -- ✅ Expected output:
    -- 20.0
    -- 6.0
    Lua runs outside the browser — try this code atonecompiler.com/luaor locally with the lua command.

    4. Default Argument Values

    Lua has no special syntax for default arguments, but there's a one-line idiom every Lua programmer uses: x = x or fallback. The or operator returns its first truthy value, so when an argument is missing (it arrives as nil) the fallback kicks in. The single gotcha: or also treats false as "missing", so for boolean flags compare against nil directly (if flag == nil then ... end) — otherwise a deliberately passed false would be overwritten.

    Worked example: the x = x or default idiom
    -- Lua has no built-in default arguments, so use the 'or' idiom:
    --   local x = x or fallback
    -- 'or' returns its first TRUTHY value, so a missing (nil) arg falls back.
    local function greet(name, greeting)
      name = name or "stranger"        -- if name is nil, use "stranger"
      greeting = greeting or "Hello"   -- if greeting is nil, use "Hello"
      return greeting .. ", " .. name .. "!"
    end
    
    print(greet())                 -- prints: Hello, stranger!
    print(greet("Ada"))            -- prints: Hello, Ada!
    print(greet("Ada", "Hi"))      -- prints: Hi, Ada!
    
    -- ⚠️ Watch out: 'or' treats false and nil the same.
    -- For boolean flags, compare to nil instead so 'false' survives:
    local function setup(verbose)
      if verbose == nil then verbose = true end   -- only default when truly missing
      return verbose
    end
    print(setup())        -- prints: true    (defaulted)
    print(setup(false))   -- prints: false   (kept!)
    Lua runs outside the browser — try this code atonecompiler.com/luaor locally with the lua command.

    5. Functions as Values & Closures

    In Lua, functions are first-class values: you can store one in a variable, pass it into another function, return it, or drop it in a table — exactly like a number or a string. This unlocks the closure: a function that remembers the local variables alive where it was created. Those captured variables stay private and persist between calls, which is how you build counters and accumulators without reaching for globals. Crucially, each closure gets its own copy of the captured state.

    Worked example: first-class functions and a counter closure
    -- Functions are FIRST-CLASS VALUES: store them, pass them, return them.
    local function square(x) return x * x end
    local f = square              -- store a function in a variable
    print(f(5))                   -- prints: 25
    
    -- Pass a function as an argument (here, apply it to every item):
    local function applyAll(fn, list)
      local out = {}
      for i, v in ipairs(list) do out[i] = fn(v) end
      return out
    end
    local doubled = applyAll(function(n) return n * 2 end, {1, 2, 3})
    print(doubled[1], doubled[2], doubled[3])   -- prints: 2   4   6
    
    -- A CLOSURE is a function that remembers variables from where it was made.
    -- 'count' lives on between calls because the inner function captured it.
    local function makeCounter()
      local count = 0
      return function()
        count = count + 1     -- updates the captured 'count'
        return count
      end
    end
    local next = makeCounter()
    print(next(), next(), next())   -- prints: 1   2   3
    
    -- Each closure gets its OWN private copy — they don't share state:
    local a, b = makeCounter(), makeCounter()
    print(a(), a(), b())            -- prints: 1   2   1
    Lua runs outside the browser — try this code atonecompiler.com/luaor locally with the lua command.

    6. Recursion

    Recursion is a function that calls itself, solving a big problem by reducing it to a smaller version of the same problem. Every recursive function needs two parts: a base case that stops the chain, and a recursive step that moves toward that base case by shrinking the input. Forget the base case and the calls never stop — you'll get a stack overflow. Recursion is the natural fit for anything nested: folder trees, JSON, or maths like factorial defined in terms of itself.

    Worked example: factorial and countdown
    -- RECURSION: a function that calls itself, shrinking the problem each time.
    -- Every recursion needs a BASE CASE so it eventually stops.
    local function factorial(n)
      if n <= 1 then return 1 end        -- base case: stop here
      return n * factorial(n - 1)        -- recursive step: gets smaller
    end
    print(factorial(5))    -- prints: 120   (5*4*3*2*1)
    
    -- Recursion shines on nested data, like counting files in a folder tree.
    local function countdown(n)
      if n == 0 then
        print("liftoff!")
        return
      end
      print(n)
      countdown(n - 1)     -- call self with a smaller number
    end
    countdown(3)
    -- prints: 3
    -- prints: 2
    -- prints: 1
    -- prints: liftoff!
    Lua runs outside the browser — try this code atonecompiler.com/luaor locally with the lua command.

    Pro Tips

    • 💡 Default to local function. A global function is namespace pollution unless you truly need it everywhere — and the local form lets the function call itself for recursion.
    • 💡 Put multi-return calls last. If you only got the first value back, your call wasn't the final item in the list. Wrap a call in (parentheses) to force exactly one value.
    • 💡 x = x or default handles missing arguments in one line — but use == nil for boolean flags so a real false isn't clobbered.
    • 💡 Closures replace globals for state. Need a counter or a running total? Capture it in a closure instead of leaking a global variable.

    Common Errors (and the fix)

    • Only the first return value shows up: a multi-return call gets truncated to one value unless it's last in the list — print(pair(), 99) drops pair()'s second value. Fix: put the call last, or assign all values with local a, b = pair().
    • "'end' expected (to close 'function' ...)": you forgot the end that closes the function (or an if/for inside it). Fix: every function, if, and for needs a matching end.
    • A function "works" but pollutes globals: writing function helper() without local creates a global that any library can overwrite. Fix: write local function helper().
    • "stack overflow": a recursive function with no reachable base case calls itself forever. Fix: add a base case (e.g. if n <= 1 then return ... end) and make sure each call shrinks the input.

    📋 Quick Reference

    TaskCodeResult / Note
    Local functionlocal function f(x) endscoped to block
    Anonymous functionlocal f = function(x) endstored in a variable
    Multiple returnreturn a, blocal x, y = f()
    Varargsfunction f(...) end{...} packs them
    Count varargsselect("#", ...)how many passed
    Default argumentx = x or 0fallback when nil
    Force one value(f())truncates to 1

    Frequently Asked Questions

    Q: Why do I only get the first value back from a multi-return function?

    A function call is trimmed to a single value whenever it is NOT the last item in a list. So print(pair(), 99) keeps only pair()'s first value, while print(99, pair()) keeps them all because the call sits last. To capture several returns, assign them: local a, b = pair(). To force just one value anywhere, wrap the call in parentheses — (pair()) is always exactly one value.

    Q: What is the difference between local function f() and function f()?

    Both create a function, but 'function f()' creates a GLOBAL named f that the entire program and every library can see and overwrite. 'local function f()' keeps f scoped to the current block, which is safer and slightly faster to call. Default to local: a global function is just namespace pollution unless you specifically need it everywhere. 'local function' also lets the function refer to itself for recursion.

    Q: How do I give a function a default argument value?

    Lua has no built-in defaults, so use the 'or' idiom inside the function: write 'name = name or "stranger"'. Because 'or' returns its first truthy operand, a missing (nil) argument falls back to your default. One catch: 'or' also rejects false, so for boolean flags compare to nil instead — 'if flag == nil then flag = true end' — otherwise passing false would be overwritten.

    Q: What exactly is a closure, and why is it useful?

    A closure is a function bundled with the outer local variables it referenced when it was created — those variables stay alive and private after the outer function returns. That lets you build counters, accumulators, and bank-account style state without globals: each call to makeCounter() returns a fresh closure with its own private 'count'. Two closures made separately do not share state.

    Q: When should I use recursion instead of a loop?

    Reach for recursion when the data is itself nested or tree-shaped — walking a folder tree, traversing JSON, or maths defined in terms of itself like factorial. Every recursion needs a BASE CASE that stops it, plus a step that makes the problem smaller each call; miss either and you get a 'stack overflow'. For simple repetition over a list, a plain for-loop is usually clearer and lighter.

    Q: What do the three dots (...) mean in a parameter list?

    '...' is the vararg expression: it collects any extra arguments the caller passed. Capture them with {...} to make a table, count them with select("#", ...), or grab from a position with select(i, ...). When some arguments might be nil, use table.pack(...) instead of {...} because it stores an accurate count in the .n field, which {...} and # cannot guarantee.

    Mini-Challenge: Bank Account Closure

    No blanks this time — just a brief and an outline to keep you on track. You'll combine three of this lesson's ideas: the default-argument idiom, returning multiple functions, and closures that share captured state. Build it, run it, and check your output against the example in the comments.

    🎯 Mini-Challenge: build a bank account with closures
    -- 🎯 MINI-CHALLENGE: a bank-account closure
    -- 1. Write makeAccount(balance) that takes a starting balance.
    --    Use the default idiom so a missing balance starts at 0:
    --       balance = balance or 0
    -- 2. Inside, return TWO closures that both capture 'balance':
    --       deposit(amount)  -> adds to balance, returns the new balance
    --       withdraw(amount) -> subtracts, returns the new balance
    --    (return them as:  return deposit, withdraw)
    -- 3. Create an account, then deposit and withdraw and print the results.
    --
    -- ✅ Example run:
    --   local deposit, withdraw = makeAccount(100)
    --   print(deposit(50))    -- 150
    --   print(withdraw(30))   -- 120
    --   print(deposit(10))    -- 130   (balance persisted between calls!)
    
    -- your code here
    Lua runs outside the browser — try this code atonecompiler.com/luaor locally with the lua command.

    🎉 Lesson Complete!

    • ✅ Define functions with function name(args) ... end; prefer local function
    • ✅ Return several values with return a, b — and a call keeps them all only when it's last in a list
    • ✅ Accept any number of arguments with varargs ...; count with select("#", ...), pack safely with table.pack
    • ✅ Give defaults with x = x or fallback (use == nil for boolean flags)
    • ✅ Functions are first-class values; closures capture and remember local state
    • ✅ Recursion solves nested problems — always include a base case to avoid a stack overflow
    • Next lesson: Control Flow and Loops — make decisions and repeat work

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service