Cheatsheet Lua
Linguagem de scripting leve para jogos, embedded e automação
Lua
Basic and Types
Variables and Types
-- Local variables (recommended):
local name = "Anna"
local age = 30
local pi = 3.14159
local active = true
local nothing = nil
-- Global variables (avoid!):
counter = 0 -- goes to _G
-- Types (type() returns a string):
type("hello") -- "string"
type(42) -- "number"
type(true) -- "boolean"
type(nil) -- "nil"
type({}) -- "table"
type(print) -- "function"Lua has dynamic typing with 8 types: nil, boolean, number, string, table, function, userdata and thread. The local keyword is essential — variables without it are global (stored in _G), causing bugs and worse performance.
Type Conversion
-- tostring (any type -> string):
tostring(42) -- "42"
tostring(true) -- "true"
tostring(nil) -- "nil"
-- tonumber (string -> number):
tonumber("42") -- 42
tonumber("3.14") -- 3.14
tonumber("0xFF") -- 255 (hex!)
tonumber("abc") -- nil (fails!)
tonumber("42", 8) -- 34 (base 8)
-- Automatic coercion:
"10" + 5 -- 15 (string -> number)
"10" .. 5 -- "105" (number -> string)
-- Check before using:
local n = tonumber(input)
if n then
print("Number: " .. n)
endLua does automatic coercion in arithmetic contexts ("10" + 5 = 15) and concatenation. tonumber() returns nil when the conversion fails — always check before using. tostring() is safe for any type. type() returns the type name the a string for validation.
Constants and Conventions
-- Lua has no native const -- Convention: UPPER_CASE for constants local MAX_SIZE = 100 local PI = 3.14159 local APP_NAME = "MyApp" -- Naming conventions: local myVariable = 10 -- camelCase local my_variable = 10 -- snake_case local _private = "internal" -- _ prefix -- Reserved names (keywords): -- and, break, do, else, elseif, end -- false, for, function, goto, if -- in, local, nil, not, or, repeat -- return, then, true, until, while -- Avoid: names with uppercase _ -- (reserved for internal variables)
Lua has no const keyword — use the UPPER_CASE convention for values that never change. Names starting with _ indicate internal/private use by convention. Keywords like local, function, return cannot be used the identifiers. Lua is case-sensitive.
Multiple Assignment
-- Simultaneous assignment: local x, y, z = 1, 2, 3 local a, b = 10, 20 -- Swap without a temp variable: a, b = b, a -- a=20, b=10 -- Extra values are discarded: local m, n = 1, 2, 3 -- 3 ignored -- Missing values become nil: local p, q, r = 1, 2 -- r = nil -- Multiple returns from functions: local ok, err = pcall(func)
Multiple assignment evaluates all values on the right before assigning — it allows an elegant swap without a temp variable. Excess values are discarded and missing ones get nil. It is idiomatic for capturing multiple returns from functions like pcall and io.open.
Input and Output
-- Output:
print("Hello World") -- with newline
print("x:", 10, "y:", 20) -- tab between args
io.write("no newline") -- without \n
-- Formatting:
print(string.format("%s is %d years old", name, 30))
-- User input:
io.write("Name: ")
local name = io.read() -- full line
local num = io.read("*n") -- number
local all = io.read("*a") -- whole file
-- Command-line arguments:
-- lua script.lua arg1 arg2
print(arg[0]) -- "script.lua"
print(arg[1]) -- "arg1"print() accepts multiple arguments separated by tab and adds a newline automatically. io.write() gives fine control (no newline). io.read() with no arguments reads a line; "*n" parses a number directly. The global arg table holds AS CLI arguments.
type() and Checking
-- type() returns the type name:
type("hello") -- "string"
type(42) -- "number"
type(true) -- "boolean"
type(nil) -- "nil"
type({}) -- "table"
type(print) -- "function"
type(io.open) -- "function"
-- Type validation:
local function process(x)
if type(x) ~= "number" then
error("expected number, got " .. type(x))
end
return x * 2
end
-- Dispatch by type:
local function describe(v)
local t = type(v)
if t == "number" then return "num: " .. v
elseif t == "string" then return "str: " .. v
else return t end
endtype() returns the type name the a string — essential for validation and dispatch. Combine with error() to fail fast with a clear message. To check for an integer in Lua 5.3+, use math.type(x) == "integer". tostring() and tonumber() complement type introspection.
Numbers and Math
-- Literals: local dec = 42 local float = 3.14 local hex = 0xFF -- 255 local exp = 1.5e3 -- 1500.0 -- Operators (Lua 5.3+): 10 / 3 -- 3.333... (always float!) 10 // 3 -- 3 (integer division) 10 % 3 -- 1 (module) 10 ^ 3 -- 1000.0 (power, float) -- math library: math.floor(3.7) -- 3 math.ceil(3.2) -- 4 math.abs(-5) -- 5 math.max(1, 5, 3) -- 5 math.sqrt(16) -- 4.0 math.random(1, 100) -- integer 1-100 math.pi -- 3.14159...
In Lua, / is always float division — for integer division use // (Lua 5.3+). ^ always returns float. Since Lua 5.3 there are integer (64-bit) and float (double) subtypes, with automatic conversion. Use math.randomseed for reproducibility.
Scope and Blocks
-- Blocks (do...end): do local temp = 42 print(temp) -- 42 end print(temp) -- nil! (out of scope) -- Shadowing: local x = 1 do local x = 2 -- new x, hides outer print(x) -- 2 end print(x) -- 1 -- Upvalues (foundation of closures): local counter = 0 local function inc() counter = counter + 1 -- upvalue end -- _ENV (environment in Lua 5.2+): -- globals are fields of _ENV
Lua uses lexical scope with blocks delimited by do...end, if...end, for...end. A local inside a block dies at the end — preventing namespace pollution. Upvalues are local variables captured by nested functions. Since Lua 5.2, _ENV enables sandboxing.
Basic Strings
-- Single or double quotes:
local s1 = "Hello World"
local s2 = 'Also works'
-- Long string (multi-line):
local text = [[
Line 1
Line 2 without escape
]]
-- Concatenation (..):
local msg = "Hello, " .. name .. "!"
local n = "age: " .. 30 -- auto-convert
-- Length:
#s1 -- 11 (bytes)
-- Repetition:
string.rep("ab", 3) -- "ababab"Strings in Lua are immutable — operations create new strings. The .. operator concatenates and converts numbers automatically. Long strings [[ ]] preserve formatting without escapes — ideal for templates. #string returns the length in bytes (not UTF-8 characters).
nil and Truthiness
-- nil: absence of value
local x = nil
print(x) -- nil
-- FALSE in Lua: ONLY nil and false!
if nil then end -- does not run
if false then end -- does not run
-- TRUE: everything else!
if 0 then print("0 is true!") end
if "" then print("empty is true!") end
if {} then print("{} is true!") end
-- nil the "removal":
local t = {a = 1, b = 2}
t.a = nil -- "removes" field a
-- Check existence:
if t.b ~= nil then
print("b exists")
endIn Lua, only nil and false are falsy — 0, empty string and empty table are ALL true (unlike Python/JS). nil is used to "remove" table fields and indicate absence of value. Always use ~= nil to check for explicit existence.
Functions
Definition and Calling
-- Local function (recommended):
local function greeting(name)
return "Hello, " .. name .. "!"
end
print(greeting("Anna")) -- "Hello, Anna!"
-- Alternative syntax:
local double = function(x)
return x * 2
end
-- No return: returns nil
local function log(msg)
print("[LOG] " .. msg)
end
-- Global function (avoid):
function global_fn()
return 42
end
-- Call with special syntax:
print "Hello" -- no parentheses (1 string arg)
dofile "script.lua"Functions in Lua are first-class values — they can be stored, passed and returned. local function is preferable to global. Without an explicit return, the function returns nil. Lua allows omitting parentheses when the only argument is a string literal or table constructor: print "Hello".
Functions the Values
-- Dispatch table:
local ops = {
add = function(a, b) return a + b end,
sub = function(a, b) return a - b end,
mul = function(a, b) return a * b end,
}
print(ops.add(3, 4)) -- 7
-- Higher-order function:
local function apply(fn, list)
local result = {}
for i, v in ipairs(list) do
result[i] = fn(v)
end
return result
end
local doubles = apply(function(x)
return x * 2
end, {1, 2, 3})
-- Callback in sort:
table.sort(nums, function(a, b)
return a > b -- descending
end)Functions the first-class values enable dispatch tables, higher-order functions and callbacks. table.sort with a custom comparator is the most common example. The obj:method() syntax is sugar for obj.method(obj) — it passes self automatically. Inline anonymous functions are ubiquitous in Lua.
Methods and self (:)
-- Definition with : (implicit self):
local Account = {}
Account.__index = Account
function Account.new(balance)
return setmetatable({balance = balance}, Account)
end
-- : passes self automatically:
function Account:deposit(v)
self.balance = self.balance + v
end
function Account:get_balance()
return self.balance
end
-- Call with :
local c = Account.new(100)
c:deposit(50)
print(c:get_balance()) -- 150
-- Equivalence:
-- c:deposit(50) == c.deposit(c, 50)
-- function Account:fn() == function Account.fn(self)The : (colon) syntax passes self automatically: obj:method() is equivalent to obj.method(obj). In the definition, function Obj:fn() equals function Obj.fn(self). It is the universal convention for methods in Lua — without :, self would be nil and the method would fail.
Multiple Returns
-- Return multiple values:
local function divide(a, b)
if b == 0 then
return nil, "Division by zero"
end
return a / b, nil
end
local result, err = divide(10, 3)
if err then
print("Error: " .. err)
else
print("Result: " .. result)
end
-- io.open pattern:
local f, err = io.open("file.txt", "r")
if not f then
print("Failed: " .. err)
return
end
-- Adjusting the number of returns:
local a, b = func() -- captures 2
local x = func() -- captures only 1
local t = {func()} -- captures allMultiple returns are idiomatic in Lua: the value, error pattern (nil + message on failure) avoids exceptions for expected errors. io.open, pcall and string.find use this pattern. Assignment captures exactly the number of variables — use {fn()} to capture all into a table.
Recursion and Tail Calls
-- Simple recursion: local function factorial(n) if n <= 1 then return 1 end return n * factorial(n - 1) end -- Tail call (optimized by Lua!): local function fat_tail(n, acc) acc = acc or 1 if n <= 1 then return acc end return fat_tail(n - 1, n * acc) -- ^ tail call: no extra stack! end -- NOT a tail call (operation after): local function fat_bad(n) if n <= 1 then return 1 end return n * fat_bad(n - 1) -- * after! end -- print(fat_tail(100000)) -- OK! -- print(fat_bad(100000)) -- stack overflow!
Lua optimizes tail calls: if the last action is return func(args), the stack frame is reused — infinite recursion without overflow. To be a tail call, the return must be EXACTLY the call — no operations afterwards. This enables loops via recursion with iteration performance. Essential for deep recursive somethingrithms.
Varargs (...)
-- Variable number of arguments:
local function sum(...)
local args = {...} -- table of args
local total = 0
for _, v in ipairs(args) do
total = total + v
end
return total
end
print(sum(1, 2, 3, 4)) -- 10
-- select to access ...:
local n = select("#", ...) -- safe count
local first = select(1, ...) -- 1st arg
-- table.pack preserves nils (5.3+):
local safe = table.pack(...)
print(safe.n) -- real count
-- Forward arguments:
local function log(fn, ...)
print("calling...")
return fn(...) -- forwards all
end
-- printf-style:
local function printf(fmt, ...)
io.write(string.format(fmt, ...))
end... (varargs) captures extra arguments. {...} creates a table but loses trailing nils — use select("#", ...) or table.pack for a safe count. fn(...) forwards all arguments. The string.format(fmt, ...) pattern is the base of printf-style functions in Lua.
Custom Iterators
-- Iterator with a closure:
local function range(n)
local i = 0
return function()
i = i + 1
if i <= n then return i end
end
end
for i in range(5) do
print(i) -- 1, 2, 3, 4, 5
end
-- Sorted pairs:
local function sorted_pairs(t)
local keys = {}
for k in pairs(t) do
keys[#keys + 1] = k
end
table.sort(keys)
local i = 0
return function()
i = i + 1
local k = keys[i]
if k then return k, t[k] end
end
end
for k, v in sorted_pairs(config) do
print(k, v)
endIterators in Lua are functions that return the next value on each call — when they return nil, the generic for stops. Closures with local state are the most common pattern. The generic for calls the iterator repeatedly until nil. They enable infinite sequences, filters and lazy transformations.
Closures
-- Closure: function + captured environment
local function make_counter()
local count = 0 -- upvalue
return function()
count = count + 1
return count
end
end
local c1 = make_counter()
local c2 = make_counter()
print(c1()) -- 1
print(c1()) -- 2
print(c2()) -- 1 (independent!)
-- Closure with parameters:
local function multiplier(factor)
return function(x)
return x * factor
end
end
local double = multiplier(2)
local triple = multiplier(3)
print(double(5)) -- 10
print(triple(5)) -- 15Closures capture upvalues (local variables of the outer scope) by reference — each call creates an independent environment. They allow private state without classes: local variables are inaccessible from outside. They are the main encapsulation tool in Lua and the foundation of callbacks, iterators and modules.
pcall and xpcall
-- pcall (protected call):
local ok, result = pcall(function()
return dangerous_operation()
end)
if ok then
print("Success: " .. result)
else
print("Error: " .. result)
end
-- pcall with a direct function:
local ok, err = pcall(io.open, "file.txt")
-- xpcall with a custom handler:
local ok, err = xpcall(func, function(e)
return e .. "\n" .. debug.traceback("", 2)
end)
-- assert (error if false):
local f = assert(io.open("data.txt"))
local n = assert(tonumber(input), "Not a number")
-- error() raises an error:
if age < 0 then
error("Negative age", 2)
endpcall runs in protected mode: it returns true+result on success, false+message on error — it is the try/catch of Lua. xpcall adds a custom handler (e.g. stack trace via debug.traceback). assert() is sugar for "if false, error()". Use error() for programming errors.
Flow Control
If / Elseif / Else
-- Basic conditional:
if grade >= 18 then
print("Excellent")
elseif grade >= 10 then
print("Passed")
else
print("Failed")
end
-- elseif is a single keyword (not else if)
-- No native switch/case:
-- Use if/elseif or a dispatch table:
local actions = {
["start"] = function() start() end,
["stop"] = function() stop() end,
}
local fn = actions[command]
if fn then fn() endelseif is a single keyword in Lua (not a separate else if). There is no native switch/case — use if/elseif chains or dispatch tables (map commands to functions). The condition can be any value: only nil and false skip the block.
Generic For (ipairs/pairs)
-- ipairs: arrays in sequential order
local fruits = {"apple", "banana", "grape"}
for i, v in ipairs(fruits) do
print(i, v) -- 1 apple, 2 banana...
end
-- STOPS at the first nil!
-- pairs: all pairs (no order)
local person = {name="Anna", age=30}
for k, v in pairs(person) do
print(k, v)
end
-- Iterate keys only:
for k in pairs(person) do
print(k)
end
-- next() manually:
local k, v = next(person)
-- Iterate a file line by line:
for line in io.lines("data.txt") do
print(line)
endipairs iterates arrays in order 1,2,3... and STOPS at the first nil — ideal for sequential lists. pairs iterates ALL key-value pairs with no guaranteed order. The generic for accepts any iterator: io.lines, string.gmatch, coroutines. The iteration variable is local and read-only.
Dispatch Tables
-- Replaces switch/case:
local handlers = {
["GET"] = function(req) return list(req) end,
["POST"] = function(req) return create(req) end,
["PUT"] = function(req) return update(req) end,
["DELETE"] = function(req) return remove(req) end,
}
local function router(req)
local handler = handlers[req.method]
if not handler then
return 405, "Method not allowed"
end
return handler(req)
end
-- With a default:
local result = (handlers[cmd] or function()
print("Unknown command: " .. cmd)
end)(arg)
-- Dynamic registration:
handlers["PATCH"] = function(req)
return patch(req)
endDispatch tables map keys to functions — they replace switch/case with advantages: dynamic registration, O(1) lookup, extensibility. The (handlers[cmd] or default)(args) pattern executes with a fallback. They are ubiquitous in Lua: HTTP routers, parsers, command patterns and state machines.
While
-- while: condition BEFORE the body
local n = 10
while n > 0 do
print(n)
n = n - 1
end
-- Infinite loop (game loop, server):
while true do
local event = wait_event()
if event == "quit" then break end
process(event)
end
-- while with multiple conditions:
local i, j = 1, 100
while i < j do
i = i + 1
j = j - 1
end
-- Careful: may never run
local x = 0
while x > 10 do
print("never shown")
endwhile checks the condition BEFORE running — the body may never run if the condition is false initially. For infinite loops, use while true do with break to exit. The condition is re-evaluated on every iteration. Body variables are local to each iteration.
Break and goto
-- break: exits the innermost loop
for i = 1, 100 do
if i * i > 50 then
print("found: " .. i)
break
end
end
-- break must be the last statement:
while true do
if condition then
break -- OK (end of the if block)
end
end
-- goto (Lua 5.2+): exit nested loops
for i = 1, 10 do
for j = 1, 10 do
if i * j > 50 then
goto found
end
end
end
::found::
print("Found!")
-- goto for retry:
::retry::
local ok, err = pcall(operation)
if not ok then goto retry endbreak can only be the last statement of a block — it exits only the innermost loop. goto (Lua 5.2+) allows jumping to ::name:: labels — useful for breaking out of nested loops and retry patterns. Labels cannot jump over local declarations. Use in moderation.
Repeat-Until
-- repeat-until: condition AFTER the body
-- Runs at least once!
local input
repeat
io.write("Password: ")
input = io.read()
until input == "secret"
-- Advantage: variables visible in until
repeat
local x = calculate()
until x > 100 -- x visible here!
-- Equivalent while would need:
local x
while true do
x = calculate()
if x > 100 then break end
end
-- until inverts: stops when TRUE
-- (continues while FALSE)repeat-until runs at least once (condition checked AFTER). Big advantage: variables declared in the body are visible in the until condition. The logic is inverted — the loop STOPS when the condition is true. Ideal for input validation and "try until it works" loops.
Loop Patterns
-- Accumulator:
local sum = 0
for i = 1, 100 do
sum = sum + i
end
-- Search with a flag:
local found = false
for _, v in ipairs(list) do
if v == target then
found = true
break
end
end
-- Manual filter:
local evens = {}
for _, v in ipairs(nums) do
if v % 2 == 0 then
evens[#evens + 1] = v
end
end
-- Loop with index and value:
for i, v in ipairs(items) do
print(i .. ": " .. v)
end
-- Countdown with step:
for i = #t, 1, -1 do
process(t[i])
endCommon patterns: accumulator (sum/concatenation), search with break, manual filter with #t+1 for append. Iterating backwards (for i = #t, 1, -1) is safe for removal during iteration. Lua has no built-in map/filter/reduce — they are implemented with simple loops.
Numeric For
-- for i = start, stop, step:
for i = 1, 10 do
print(i) -- 1, 2, ..., 10
end
-- With negative step (countdown):
for i = 10, 1, -1 do
print(i)
end
-- With decimal step:
for i = 0, 1, 0.1 do
print(string.format("%.1f", i))
end
-- The variable is LOCAL and read-only:
for i = 1, 5 do
i = i * 2 -- does NOT affect the loop!
end
-- start, stop, step evaluated once:
local limit = 10
for i = 1, limit do
limit = 0 -- does not affect iterations
endThe numeric for has the form for i = start, stop, step do. The default step is 1; it can be negative or decimal. The iteration variable is local and read-only — reassigning does not affect the loop. Start, stop and step are evaluated ONCE before starting. The loop includes the final value (it is inclusive).
Guard Clauses and Early Return
-- Instead of deep nesting:
local function process(data)
if not data then return nil, "no data" end
if #data == 0 then return {} end
if type(data[1]) ~= "number" then
return nil, "invalid type"
end
-- Main logic (no nesting):
local result = {}
for _, v in ipairs(data) do
result[#result + 1] = v * 2
end
return result
end
-- Validation with assert:
local function divide(a, b)
assert(type(a) == "number", "a must be a number")
assert(b ~= 0, "division by zero")
return a / b
endGuard clauses check error conditions at the start and return early — they avoid deep nesting and make the code more readable. The return nil, msg pattern for operational errors is idiomatic. assert() validates programming preconditions (fail fast). Combine with pcall in the caller to catch.
Strings e Patterns
string library
-- Basic operations:
local s = "Hello World"
string.upper(s) -- "HELLO WORLD"
string.lower(s) -- "hello world"
string.len(s) -- 11 (bytes)
string.rep("ab", 3) -- "ababab"
string.reverse("abc") -- "cba"
-- Substring:
string.sub(s, 1, 5) -- "Hello"
string.sub(s, 7) -- "World" (to the end)
string.sub(s, -5) -- "World" (from the end)
-- Byte/char:
string.byte("A") -- 65
string.char(65, 66) -- "AB"
-- Method with : (sugar):
local u = s:upper() -- equivalent
local p = s:sub(1, 5)The string library offers immutable operations — all return a new string. It can be called the a function (string.upper(s)) or the a method (s:upper()). string.sub accepts negative indices (from the end). string.byte/string.char convert between bytes and characters. Strings are always immutable in Lua.
string.gsub
-- Global substitution:
local s = "Hello World World"
local r, n = string.gsub(s, "World", "Lua")
print(r) -- "Hello Lua Lua"
print(n) -- 2 (substitutions)
-- Limit substitutions:
string.gsub(s, "World", "Lua", 1)
-- "Hello Lua World" (only 1st)
-- With patterns:
string.gsub("a1b2c3", "%d", "#")
-- "a#b#c#"
-- With a function (transformation):
string.gsub("hello world", "%a+", function(w)
return w:upper()
end)
-- "HELLO WORLD"
-- With a table (lookup):
local entities = {lt="<", gt=">", amp="&"}
string.gsub("<>", "&(%a+);", entities)
-- "<>"string.gsub replaces all occurrences and returns the string + count. The 4th argument limits substitutions. It can receive a function (the function result replaces) or a table (key → value) the replacement. It is the most powerful string transformation tool in Lua — it replaces regex replace from other languages.
string.format
-- printf-style formatting:
string.format("%s is %d years old", "Anna", 30)
-- "Anna is 30 years old"
-- Specifiers:
string.format("%.2f", 3.14159) -- "3.14"
string.format("%10s", "Lua") -- " Lua"
string.format("%-10s|", "Lua") -- "Lua |"
string.format("%05d", 42) -- "00042"
string.format("%x", 255) -- "ff"
string.format("%X", 255) -- "FF"
string.format("%o", 8) -- "10" (octal)
string.format("%c", 65) -- "A"
string.format("%q", 'he said "hi"')
-- "he said \"hi\"" (escaped)
-- %% for literal percent:
string.format("%.1f%%", 85.678) -- "85.7%"
-- %d integers only (5.3+):
string.format("%d", 3.7) -- ERROR!string.format is the printf of Lua: %d integer, %s string, %f float, %x hex, %c char. %.2f limits decimal places. %q escapes into a safe Lua literal. %% for a literal percent. In Lua 5.3+, %d requires an integer — use %f or math.floor for floats.
Captures and Parsing
-- Captures with parentheses:
local key, val = string.match(
"name=Anna", "(%a+)=(%a+)")
print(key, val) -- "name", "Anna"
-- URL parsing:
local proto, host, path = string.match(
"https://site.com/page",
"(%a+)://([^/]+)(.*)")
-- Simple CSV:
for field in string.gmatch("a,b,c", "[^,]+") do
print(field) -- a, b, c
end
-- gmatch: match iterator
for word in string.gmatch("one two three", "%a+") do
print(word)
end
-- Positional capture:
local h, m = string.match("14:30", "(%d+):(%d+)")
print(h, m) -- 14, 30
-- %0 captures the whole match:
string.gsub("abc", "(%a)", "[%0]")
-- "[a][b][c]"Captures with () extract parts of the match — string.match returns all captures. string.gmatch is an iterator over all matches (ideal for parsing CSV, tokens). %0 refers to the whole match. Patterns like [^,]+ (one or more non-comma) are common for splitting. Simpler than regex for typical cases.
string.find and match
-- find: returns positions (or nil)
local ini, fin = string.find("Hello World", "World")
print(ini, fin) -- 7, 11
-- With patterns:
local i, j = string.find("price: 42", "%d+")
print(i, j) -- 8, 9
-- match: extracts the found pattern
local num = string.match("age: 30", "%d+")
print(num) -- "30"
-- Multiple captures:
local d, m, y = string.match(
"25/12/2024", "(%d+)/(%d+)/(%d+)")
print(d, m, y) -- 25, 12, 2024
-- No match: returns nil
local r = string.match("abc", "%d+")
print(r) -- nil
-- plain text (no patterns):
string.find("a.b", ".", 1, true) -- 2, 2string.find returns start/end positions (or nil). string.match extracts the found content — more convenient for parsing. Captures with () return multiple values. The 4th argument true in find disables patterns (literal search). Both accept a start position the an extra argument.
string.gmatch and split
-- gmatch: pattern iterator
for word in string.gmatch("one two three", "%S+") do
print(word)
end
-- Split by delimiter:
local function split(s, sep)
local parts = {}
for part in string.gmatch(s, "[^" .. sep .. "]+") do
parts[#parts + 1] = part
end
return parts
end
local t = split("a,b,c", ",")
-- {"a", "b", "c"}
-- Simple tokenizer:
for token in string.gmatch(code, "%a+%d*") do
print(token)
end
-- Extract all numbers:
local nums = {}
for n in string.gmatch("x1 y22 z333", "%d+") do
nums[#nums + 1] = tonumber(n)
end
-- {1, 22, 333}string.gmatch returns an iterator over all matches — ideal for splitting, tokenizing and multiple extraction. The [^sep]+ (non-delimiter) pattern is the base of split functions. %S+ captures words (non-space). Unlike match (first only), gmatch walks the whole string. Essential for parsing in Lua.
Lua Patterns
-- Patterns (NOT regex!):
-- %a = letter, %d = digit, %s = space
-- %w = alphanumeric, %p = punctuation
-- %u = uppercase, %l = lowercase
-- Quantifiers:
-- + = 1 or more (greedy)
-- * = 0 or more (greedy)
-- - = 0 or more (lazy)
-- ? = 0 or 1
-- Anchors:
-- ^ = start of string
-- $ = end of string
-- Examples:
string.match("abc123", "%a+") -- "abc"
string.match("abc123", "%d+") -- "123"
string.match("a.b", "%.") -- "." (escape)
string.match("hello", "^h") -- "h"
string.match("file.lua", "%.%w+$") -- "lua"
-- Uppercase classes = complement:
-- %A = non-letter, %D = non-digitLua patterns are simpler than regex: %a letter, %d digit, %s space, %w alphanumeric. Quantifiers: + (greedy), - (lazy), *, ?. % escapes special characters. Uppercase classes are complements (%D = non-digit). They do not support alternation (|) nor complex groups.
utf8 (Lua 5.3+)
-- # returns BYTES, not characters:
#"café" -- 5 (é = 2 UTF-8 bytes)
-- utf8 library (5.3+):
local utf8 = require("utf8") -- or built-in
-- Length in characters:
utf8.len("café") -- 4
-- Iterate codepoints:
for pos, code in utf8.codes("café") do
print(pos, code)
end
-- Convert:
utf8.char(65, 66) -- "AB"
utf8.codepoint("is") -- 233
-- Offset (byte position):
utf8.offset("café", 4) -- byte pos of 4th char
-- Substring by character:
local function usub(s, i, j)
local pi = utf8.offset(s, i)
local pj = utf8.offset(s, j + 1)
return s:sub(pi, pj - 1)
endIn Lua, # returns bytes — to count UTF-8 characters use utf8.len() (Lua 5.3+). utf8.codes() iterates codepoints with positions. utf8.offset() converts a character position to a byte one (needed for string.sub). Without the utf8 library, operations like upper/lower can fail on accented characters.
Advanced
Coroutines
-- Create and run:
local co = coroutine.create(function(a, b)
print("Start:", a, b)
local c = coroutine.yield(a + b)
print("Received:", c)
return a * b
end)
print(coroutine.resume(co, 3, 4))
-- "Start: 3 4", true, 7
print(coroutine.resume(co, 10))
-- "Received: 10", true, 12
print(coroutine.status(co)) -- "dead"
-- coroutine.wrap (simple generator):
local function fibonacci()
return coroutine.wrap(function()
local a, b = 0, 1
while true do
coroutine.yield(a)
a, b = b, a + b
end
end)
end
for n in fibonacci() do
if n > 100 then break end
print(n)
endCoroutines are functions that pause (yield) and resume (resume) — cooperative, not parallel. coroutine.create + resume/yield give full control; coroutine.wrap returns an iterable function (simpler for generators). States: suspended, running, dead. Uses: infinite generators, state machines, simulated async.
Metaprogramming
-- __index the function (properties):
local Config = setmetatable({}, {
__index = function(t, key)
local env = os.getenv(key:upper())
return env or "default_" .. key
end
})
print(Config.database_url) -- reads ENV
-- __newindex (validation):
local Strict = setmetatable({}, {
__newindex = function(t, k, v)
if type(v) ~= "number" then
error(k .. " must be a number")
end
rawset(t, k, v)
end
})
Strict.x = 42 -- OK
Strict.y = "abc" -- ERROR!
-- DSL with __index + __call:
local html = setmetatable({}, {
__index = function(_, tag)
return function(content)
return "<" .. tag .. ">" .. content
.. "</" .. tag .. ">"
end
end
})
print(html.div("Hello")) -- <div>Hello</div>Metaprogramming via metamethods: __index the a function creates computed properties, __newindex validates writes, __call turns tables into callable DSLs. rawset/rawget are essential inside metamethods to avoid infinite recursion. This mechanism enables OOP, frameworks and expressive DSLs in Lua.
Modules and require
-- File: utils.lua
local M = {}
local function private() -- not exported
return "internal"
end
function M.add(a, b)
return a + b
end
return M -- MANDATORY to return
-- Usage in another file:
local utils = require("utils")
print(utils.add(3, 4)) -- 7
-- require is cached (singleton):
local u1 = require("utils")
local u2 = require("utils")
print(u1 == u2) -- true (same table)
-- package.path (where it searches):
print(package.path) -- "./?.lua;..."
-- Submodules:
local json = require("cjson")require loads and runs a module ONCE (cached in package.loaded) — subsequent calls return the same table. The module MUST return a table or function. local variables are private — only what is exported on the table is public. package.path defines where it searches (? = module name).
C API and FFI
-- Lua C API (native extensions):
-- File mylib.c:
-- #include <lua.h>
-- #include <lauxlib.h>
--
-- static int l_add(lua_State *L) {
-- double a = luaL_checknumber(L, 1);
-- double b = luaL_checknumber(L, 2);
-- lua_pushnumber(L, a + b);
-- return 1;
-- }
--
-- int luaopen_mylib(lua_State *L) {
-- lua_register(L, "add", l_add);
-- return 0;
-- }
-- LuaJIT FFI (no C!):
local ffi = require("ffi")
ffi.cdef[[
int printf(const char *fmt, ...);
]]
ffi.C.printf("Hello %s!\n", "Lua")
-- Load a C module:
local mylib = require("mylib")The C API allows extending Lua with native code — C functions receive lua_State (virtual stack), read args with luaL_check*, return via lua_push*. luaopen_name is the entry point for require. LuaJIT FFI eliminates C: ffi.cdef declares signatures and ffi.C calls system functions. Lua was designed for embedding.
File I/O
-- Read the whole file:
local f = assert(io.open("data.txt", "r"))
local content = f:read("*a")
f:close()
-- Read line by line:
for line in io.lines("data.txt") do
print(line)
end
-- Write:
local out = assert(io.open("output.txt", "w"))
out:write("Line 1\n")
out:write(string.format("Value: %d\n", 42))
out:close()
-- Append:
local log = io.open("app.log", "a")
log:write(os.date() .. " event\n")
log:close()
-- Modes: r, w, a, r+, w+, a+
-- rb, wb (binary on Windows)
-- Check existence:
local function exists(path)
local f = io.open(path, "r")
if f then f:close() return true end
return false
endio.open returns a file handle or nil + error — always use assert() or check. io.lines() is the idiomatic iterator for line-by-line reading (closes automatically). Modes: "r" read, "w" write (truncates), "a" append. On Windows, add "b" for binary.
Debug and Profiling
-- debug library:
local info = debug.getinfo(1)
print(info.source) -- "@script.lua"
print(info.currentline) -- current line
-- Stack trace:
print(debug.traceback("message"))
-- Hooks (monitor execution):
debug.sethook(function(event, line)
print(event, line)
end, "l") -- "l" = each line
-- Inspect upvalues:
local f = function()
local x = 42
return x
end
local name, val = debug.getupvalue(f, 1)
print(name, val) -- "x", 42
-- Simple profiling:
local function profile(fn, ...)
local start = os.clock()
local results = {fn(...)}
print(string.format("%.4fs", os.clock() - start))
return table.unpack(results)
end
-- Memory:
collectgarbage("count") -- KB usedThe debug library allows introspection: getinfo returns source/line, traceback generates stack traces, getupvalue/getlocal inspect variables. sethook enables profilers (event "l" = each line, "c" = call). os.clock() measures CPU time. collectgarbage("count") reports memory in KB.
Advanced Error Handling
-- result/error pattern (Go-style):
local function read_file(path)
local f, err = io.open(path, "r")
if not f then
return nil, "Could not open: " .. err
end
local content = f:read("*a")
f:close()
return content
end
local data, err = read_file("config.json")
if not data then
print("Error: " .. err)
return
end
-- Error objects (structured):
error({code = 404, msg = "Not found"})
-- Retry with pcall:
local function with_retry(fn, attempts)
attempts = attempts or 3
for i = 1, attempts do
local ok, result = pcall(fn)
if ok then return result end
if i == attempts then error(result) end
end
endLua has two styles: exceptions (error/pcall) for programming errors, and return nil, msg for operational errors. The nil+msg style is idiomatic in I/O. Error objects (tables with code/msg) allow structured errors. with_retry wraps attempts with pcall — a common pattern in networking.
OS and Environment
-- Date and time:
os.time() -- Unix timestamp
os.date("%Y-%m-%d") -- "2024-12-25"
os.date("*t") -- table with fields
os.clock() -- CPU time (seconds)
as.difftime(t2, t1) -- difference
-- Environment:
os.getenv("HOME") -- environment variable
os.getenv("PATH")
-- System:
os.execute("ls -la") -- run a command
os.remove("temp.txt") -- delete file
os.rename("a.txt", "b.txt")
os.tmpname() -- unique temp name
os.exit(0) -- terminate program
-- Date table:
local t = os.date("*t")
print(t.year, t.month, t.day)
print(t.hour, t.min, t.sec)
-- Timestamp of a specific date:
local ts = os.time({year=2024, month=12, day=25})os.time() returns a Unix timestamp; os.date() formats (with "*t" it returns a table with fields). os.clock() measures CPU time for profiling. os.getenv() reads environment variables. os.execute() runs system commands. For advanced filesystem operations (mkdir, glob), use LuaFileSystem (lfs) via luarocks.
Operators
Arithmetic Operators
-- Basics: 10 + 3 -- 13 10 - 3 -- 7 10 * 3 -- 30 10 / 3 -- 3.333... (float!) -10 -- unary negation -- Lua 5.3+: 10 // 3 -- 3 (integer division) 10 % 3 -- 1 (module/remainder) 10 ^ 3 -- 1000.0 (power) -- Module with negatives: -7 % 3 -- 2 (positive result) 7 % -3 -- -2 (sign of the divisor) -- Integer vs float: local a = 10 // 3 -- integer 3 local b = 10 / 3 -- float 3.333 local c = 2 ^ 10 -- float 1024.0
/ is always float in Lua; // does integer division (Lua 5.3+). % follows the sign of the divisor. ^ always returns float and is right-associative (2^3^2 = 2^9 = 512). Operations between integers stay integer except / and ^.
Operator Precedence
-- Order (high -> low):
-- ^
-- not # - (unary)
-- * / // %
-- + -
-- ..
-- < > <= >= ~= ==
-- and
-- or
-- Examples:
2 + 3 * 4 -- 14 (not 20)
2 ^ 3 ^ 2 -- 512 (right-assoc)
-2 ^ 2 -- -4 (unary after ^)
not nil and 42 -- 42
-- Use parentheses for clarity:
local x = (a + b) * c
local y = not (x == 0)
-- .. is right-associative:
"a" .. "b" .. "c" -- "a" .. ("b" .. "c")^ has the highest precedence and is right-associative. Unary - has lower precedence than ^ (-2^2 = -4). .. is also right-associative. and has higher precedence than or. When in doubt, use parentheses — they make the intent clear with no performance cost.
rawget, rawset and rawequal
-- rawget: read without metamethods
local t = setmetatable({}, {
__index = function() return "default" end
})
t.x -- "default" (via __index)
rawget(t, "x") -- nil (ignores __index)
-- rawset: write without metamethods
local strict = setmetatable({}, {
__newindex = function()
error("forbidden!")
end
})
strict.x = 1 -- ERROR!
rawset(strict, "x", 1) -- OK (ignores)
-- rawequal: comparison without __eq
local mt = {__eq = function() return true end}
local a = setmetatable({}, mt)
local b = setmetatable({}, mt)
a == b -- true (via __eq)
rawequal(a, b) -- false (reference)
-- rawlen (5.2+): # without __len
rawlen("hello") -- 5The raw* functions bypass metamethods: rawget/rawset are essential inside __index/__newindex to avoid infinite recursion. rawequal compares by reference ignoring __eq. rawlen (5.2+) ignores __len. Use them only when you need to access the "real" table.
Relational Operators
-- Comparisons (return boolean):
10 == 10 -- true (equal)
10 ~= 5 -- true (NOT EQUAL, not !=)
10 < 20 -- true
10 > 5 -- true
10 <= 10 -- true
10 >= 11 -- false
-- NOTE: ~= is "not equal" in Lua!
-- (there is no !=)
-- String comparison (lexicographic):
"abc" < "abd" -- true
"Lua" == "Lua" -- true
-- Tables/functions: only == and ~=
-- (compare reference, not content)
local a = {}
local b = {}
a == b -- false (different objects)
a == a -- true (same reference)The inequality operator in Lua is ~= (not !=). Comparisons < and > work for numbers and strings (lexicographic order). Tables and functions can only be compared with ==/~= by reference — for value comparison, use the __eq, __lt metamethods.
Bitwise Operators (5.3+)
-- Bitwise (Lua 5.3+):
0xFF & 0x0F -- 15 (AND)
0xF0 | 0x0F -- 255 (OR)
0xFF ~ 0x0F -- 240 (XOR)
~0xFF -- -256 (NOT, complement)
-- Shifts:
1 << 4 -- 16 (shift left)
256 >> 4 -- 16 (shift right)
-- Applications:
-- Flags/permissions:
local READ = 1 -- 001
local WRITE = 2 -- 010
local EXEC = 4 -- 100
local perms = READ | WRITE -- 3 (011)
-- Check a flag:
if perms & READ ~= 0 then
print("has read")
endBitwise operators exist since Lua 5.3: & (AND), | (OR), ~ (binary XOR / unary NOT), << and >> (shifts). They operate only on integers. Useful for flags, permissions and bit manipulation. In Lua 5.1/5.2 use bit32 or bit (LuaJIT).
Logical Operators
-- and: returns 1st false or the last:
true and 42 -- 42
nil and 42 -- nil
1 and 2 and 3 -- 3
-- or: returns 1st true or the last:
nil or "default" -- "default"
false or nil or 42 -- 42
1 or 2 -- 1
-- not: returns boolean:
not nil -- true
not false -- true
not 0 -- false! (0 is true)
not "" -- false! ("" is true)
not {} -- false! ({} is true)
-- Short-circuit:
-- and stops at the 1st false
-- or stops at the 1st trueand and or in Lua do NOT return boolean — they return the operand: and returns the first false or the last; or returns the first true or the last. Both short-circuit. not is the only one that always returns true/false.
Ternary Pattern (and/or)
-- Idiomatic ternary:
local status = active and "online" or "offline"
local name = input ~= "" and input or "Anonymous"
-- Equivalent to:
-- status = active ? "online" : "offline"
-- CAREFUL: fails if A is false/nil!
local x = true and false or "oops"
-- x = "oops" (not false!)
-- Solution with a table:
local r = (cond and {valueA} or {valueB})[1]
-- Default value (safer):
local timeout = opts.timeout or 30
local host = config.host or "localhost"
-- Conditional execution:
debug and print("debug mode on")The cond and A or B pattern simulates a ternary — it works when A is never false/nil. If A can be false, use the table trick (cond and {A} or {B})[1]. For defaults, x = value or default is the most common and safe pattern (equivalent to ?? in other languages).
Concatenation and Length
-- Concatenation (..):
"Hello" .. " " .. "World" -- "Hello World"
"age: " .. 30 -- "age: 30"
"x=" .. 3.14 -- "x=3.14"
-- Careful: glued numbers
print(10 .. 20) -- "1020" (string!)
print(10 + 20) -- 30 (sum)
-- Length (#):
#"hello" -- 5 (bytes)
#{1, 2, 3} -- 3 (array)
#"" -- 0
-- # with tables (sequential part):
local t = {10, 20, 30, name="x"}
#t -- 3 (array part only)
-- # does NOT work with nils in the middle:
local bad = {1, nil, 3}
#bad -- undefined!The .. operator concatenates strings and converts numbers automatically — 10 .. 20 produces "1020". # returns the length of strings (bytes) and the sequential part of tables. Careful: # is undefined on arrays with nil in the middle — use table.pack with the .n field to count safely.
The # Operator and Tables
-- # on strings (bytes):
#"hello" -- 5
#"café" -- 5 (UTF-8: é = 2 bytes)
-- # on arrays (sequential part):
#{10, 20, 30} -- 3
#{} -- 0
-- Append using #:
local t = {}
t[#t + 1] = "first"
t[#t + 1] = "second"
-- DANGER: # with nils in the middle
local bad = {1, 2, nil, 4}
#bad -- can be 2 or 4!
-- Solution: table.pack preserves count
local safe = table.pack(1, nil, 3)
print(safe.n) -- 3 (real count)
-- table.insert is safer:
table.insert(t, "third")#t returns the "border" of the sequential part — with nil in the middle, the result is undefined. For append, t[#t+1] = v is idiomatic but table.insert(t, v) is safer. table.pack (5.3+) preserves nils with the .n field. Never trust # for sparse arrays.
Tables
Tables the Arrays
-- Creation (1-based indexing!):
local fruits = {"apple", "banana", "grape"}
print(fruits[1]) -- "apple" (NOT [0]!)
print(fruits[3]) -- "grape"
print(#fruits) -- 3
-- Dynamic construction:
local nums = {}
for i = 1, 10 do
nums[#nums + 1] = i * i
end
-- Insertion and removal:
table.insert(fruits, "mango") -- end
table.insert(fruits, 1, "kiwi") -- position
table.remove(fruits, 2) -- removes pos 2
table.remove(fruits) -- removes last
-- Unpack:
local a, b, c = table.unpack({10, 20, 30})Tables in Lua are 1-based — the first element is [1], not [0]. #t returns the length of the sequential part. table.insert/table.remove manipulate positions; without a position, they operate at the end. table.unpack spreads elements the multiple arguments. It is the most important convention when coming from other languages.
table.concat and pack
-- concat (join with separator):
local words = {"Lua", "is", "fast"}
print(table.concat(words, " "))
-- "Lua is fast"
print(table.concat(words, ", ", 1, 2))
-- "Lua, is"
-- Efficient for large strings:
local parts = {}
for i = 1, 1000 do
parts[#parts + 1] = "line " .. i
end
local text = table.concat(parts, "\n")
-- table.pack (5.3+, preserves nils):
local t = table.pack(1, nil, 3)
print(t.n) -- 3 (real count!)
local a, b, c = table.unpack(t, 1, t.n)
-- table.move (copy ranges, 5.3+):
local arr = {1, 2, 3, 4, 5}
table.move(arr, 2, 4, 1) -- shift lefttable.concat is the efficient way to build large strings — repeated concatenation with .. is O(n²). table.pack preserves nils with the .n field ({...} loses trailing nils). table.move (5.3+) copies ranges efficiently. Use concat whenever building output with many parts.
Filter, map and reduce
-- Lua has no built-in, implement them:
-- Filter:
local function filter(t, pred)
local result = {}
for _, v in ipairs(t) do
if pred(v) then
result[#result + 1] = v
end
end
return result
end
-- Map:
local function map(t, fn)
local result = {}
for i, v in ipairs(t) do
result[i] = fn(v)
end
return result
end
-- Reduce:
local function reduce(t, fn, init)
local acc = init
for _, v in ipairs(t) do
acc = fn(acc, v)
end
return acc
end
local sum = reduce({1,2,3,4}, function(a, b)
return a + b
end, 0) -- 10Lua has no built-in map/filter/reduce — they are implemented the utility functions with loops. The #result+1 pattern for append is idiomatic. Libraries like Moses or lua-functional add these functions with chaining. Manual construction is simple and performant — often preferable in Lua for clarity.
Tables the Dictionaries
-- Creation with keys:
local person = {
name = "Anna",
age = 30,
["e-mail"] = "ana@mail.com",
}
-- Access:
print(person.name) -- dot notation
print(person["e-mail"]) -- bracket (special keys)
person.city = "Lisbon" -- add
person.age = nil -- "remove"
-- Iteration (no guaranteed order):
for key, value in pairs(person) do
print(key .. ": " .. tostring(value))
end
-- Check existence:
if person.email ~= nil then
print("has email")
end
-- Any value except nil the a key:
local t = {[true] = "yes", [42] = "answer"}Tables the dictionaries use any value except nil the a key. Dot notation (t.field) only works for valid identifiers — for special keys use brackets (t["e-mail"]). pairs() iterates all pairs with no guaranteed order. Setting a field to nil "removes" the entry.
Sets with Tables
-- Set: table with true values
local function new_set(list)
local set = {}
for _, v in ipairs(list) do
set[v] = true
end
return set
end
local a = new_set({1, 2, 3, 4, 5})
local b = new_set({3, 4, 5, 6, 7})
-- O(1) membership:
if a[3] then print("3 is in a") end
-- Union:
local union = {}
for k in pairs(a) do union[k] = true end
for k in pairs(b) do union[k] = true end
-- Intersection:
local inter = {}
for k in pairs(a) do
if b[k] then inter[k] = true end
end
-- Remove duplicates:
local seen = {}
local unique = {}
for _, v in ipairs(list) do
if not seen[v] then
seen[v] = true
unique[#unique + 1] = v
end
endSets in Lua are tables with true values — membership checking (set[x]) is O(1) vs O(n) for searching an array. Set operations are loops over pairs(). The "seen" pattern removes duplicates preserving order. Ideal for tracking visited state (graphs, BFS) and frequent membership checks.
Nested Tables
-- JSON-like structures:
local data = {
users = {
{ name = "Anna", age = 30 },
{ name = "Brian", age = 25 },
},
meta = { total = 2, page = 1 },
}
print(data.users[1].name) -- "Anna"
print(data.meta.total) -- 2
-- Dynamic construction:
local tree = {}
tree.children = {}
tree.children[1] = {value = "A", children = {}}
-- Safe access (avoid nil error):
local name = data.users[3]
and data.users[3].name
or "unknown"
-- Mixed tables (array + hash):
local config = {
"localhost", -- [1]
8080, -- [2]
debug = true,
name = "server",
}Tables are the ONLY data structure in Lua — they serve the array, dict, object, module and record. Nesting creates structures accessed with chained notation. Careful: accessing a field of nil raises an error — use and for safe access on optional paths. Mixed tables combine array and hash parts in the same table.
Weak Tables and Caches
-- Weak table (does not prevent GC):
local cache = setmetatable({}, {
__mode = "v" -- weak values
})
-- Memoization with a weak cache:
local memo = setmetatable({}, {__mode = "kv"})
local function fib(n)
if memo[n] then return memo[n] end
local result
if n < 2 then result = n
else result = fib(n-1) + fib(n-2) end
memo[n] = result
return result
end
-- __mode options:
-- "k" = weak keys
-- "v" = weak values
-- "kv" = both weak
-- When the original object is freed,
-- the entry disappears automaticallyWeak tables let the garbage collector remove entries when keys/values are not referenced elsewhere. __mode = "v" makes values weak, "k" keys, "kv" both. They enable caches without memory leaks — when the object is freed, the entry disappears. Essential for memoization and object pools.
table.sort
-- Ascending sort (in-place):
local nums = {5, 3, 8, 1, 9}
table.sort(nums) -- {1, 3, 5, 8, 9}
-- Sort with a comparator:
table.sort(nums, function(a, b)
return a > b -- descending
end)
-- Sorting records:
local people = {
{name = "Anna", age = 30},
{name = "Brian", age = 25},
{name = "Carla", age = 28},
}
table.sort(people, function(a, b)
return a.age < b.age
end)
-- Stable sort (add an index):
for i, v in ipairs(people) do
v._idx = i
end
table.sort(people, function(a, b)
if a.age == b.age then
return a._idx < b._idx
end
return a.age < b.age
end)table.sort is in-place and uses quicksort — the comparator must return true if a comes before b. Stability is NOT guaranteed — for a stable sort, add the original index the a tiebreaker. The comparator must be consistent (total order). Do not modify the table during AS sort.
Swap-remove and Performance
-- O(1) removal (does not preserve order):
local function remove_fast(t, i)
t[i] = t[#t] -- last into position
t[#t] = nil -- remove last
end
-- vs table.remove (O(n), preserves order):
table.remove(t, i) -- shifts everything
-- Pre-allocation for performance:
local buffer = {}
for i = 1, 10000 do
buffer[i] = 0 -- pre-allocate
end
-- Avoid # in a hot loop:
local len = #t -- compute once
for i = 1, len do
process(t[i])
end
-- ipairs vs numeric for:
-- for i=1,#t is faster than ipairs
for i = 1, #t do
local v = t[i]
endSwap-remove replaces the element with the last one and removes — O(1) when order does not matter (essential in game loops). table.remove is O(n) since it shifts. In hot loops, cache #t outside the loop. for i=1,#t is slightly faster than ipairs in Lua 5.3+. Pre-allocation avoids rehashing.
Metatables e OOP
Metatables Basics
-- Metatable defines table behavior:
local mt = {}
local t = setmetatable({}, mt)
-- setmetatable returns AS table itself:
local obj = setmetatable({}, {})
-- getmetatable:
local mt2 = getmetatable(t)
-- __index (read fallback):
local defaults = {color = "white", size = 10}
local o = setmetatable({}, {__index = defaults})
print(o.color) -- "white" (from defaults)
o.color = "blue" -- creates own field
print(o.color) -- "blue" (own)
-- __newindex (intercept writes):
local strict = setmetatable({}, {
__newindex = function(t, k, v)
error("Field not allowed: " .. k)
end
})Metatables are the extension mechanism of Lua — they define how tables behave in operations. __index is consulted when a field does NOT exist — it can be a table (delegation) or a function (computed). __newindex intercepts writes of new fields. setmetatable returns the table, allowing chaining.
Standard Class Pattern
-- Universal class pattern in Lua:
local Animal = {}
Animal.__index = Animal
function Animal.new(name, sound)
local self = setmetatable({}, Animal)
self.name = name
self.sound = sound or "..."
return self
end
function Animal:speak()
return self.name .. ": " .. self.sound .. "!"
end
function Animal:__tostring()
return "Animal(" .. self.name .. ")"
end
-- Usage:
local rex = Animal.new("Rex", "Woof")
print(rex:speak()) -- "Rex: Woof!"
print(rex) -- "Animal(Rex)"
-- rex:speak() == rex.speak(rex)The class pattern uses __index pointing to the table itself: fields missing on the object are looked up in the class. Methods use : which passes self automatically. There are no native classes in Lua — this prototype-based pattern with metatables is the universal convention. Each object is a table with a metatable.
Mixin and Composition
-- Mixin: copy methods into the class
local Serializable = {}
function Serializable:to_json()
local parts = {}
for k, v in pairs(self) do
if type(v) ~= "function" then
parts[#parts + 1] = string.format(
'"%s":"%s"', k, tostring(v))
end
end
return "{" .. table.concat(parts, ",") .. "}"
end
local Loggable = {}
function Loggable:log(msg)
print("[" .. tostring(self) .. "] " .. msg)
end
-- Apply mixins:
local User = {}
User.__index = User
for k, v in pairs(Serializable) do User[k] = v end
for k, v in pairs(Loggable) do User[k] = v end
function User.new(name)
return setmetatable({name = name}, User)
endComposition via mixin copies methods from modules into classes — an alternative to multiple inheritance without __index chain complexity. Advantages: no hierarchical coupling, no diamond problem, selective composition. Downside: name conflicts are silent (last copy wins). Prefer composition when behaviors are independent.
__index and Delegation
-- __index the table (prototype):
local Animal = {sound = "...", legs = 4}
local dog = setmetatable({}, {__index = Animal})
print(dog.sound) -- "..." (inherited)
print(dog.legs) -- 4 (inherited)
dog.sound = "Woof" -- own field
print(dog.sound) -- "Woof" (override)
-- __index the function (computed):
local proxy = setmetatable({}, {
__index = function(t, key)
return "field '" .. key .. "' does not exist"
end
})
print(proxy.xyz) -- "field 'xyz' does not exist"
-- Prototype chain:
local A = {level = "A"}
local B = setmetatable({}, {__index = A})
local C = setmetatable({}, {__index = B})
print(C.level) -- "A" (C -> B -> A)__index the a table creates delegation/prototype — the lookup follows the chain until found. As a function, it allows computed properties (values calculated on demand). The chain can have multiple levels (C → B → A). It is the fundamental mechanism for OOP, modules with defaults and configuration with fallback in Lua.
Inheritance
-- Base class:
local Animal = {}
Animal.__index = Animal
function Animal.new(name)
return setmetatable({name = name}, Animal)
end
function Animal:speak()
return self.name .. " makes ..."
end
-- Derived class:
local Dog = setmetatable({}, {__index = Animal})
Dog.__index = Dog
function Dog.new(name, breed)
local self = Animal.new(name)
setmetatable(self, Dog)
self.breed = breed
return self
end
function Dog:speak() -- override
return self.name .. ": Woof woof!"
end
-- Chain: Dog -> Animal (via __index)
local rex = Dog.new("Rex", "Labrador")
print(rex:speak()) -- "Rex: Woof woof!"Inheritance is an __index chain: Dog.__index = Dog and the metatable of Dog has __index = Animal. Lookup: rex → Dog → Animal. Override is simple: defining a method on Dog hides the one from Animal. To call the "superclass", use Animal.speak(self) explicitly — there is no native super.
Arithmetic Metamethods
-- Vector with metamethods:
local Vector = {}
Vector.__index = Vector
function Vector.new(x, y)
return setmetatable({x = x, y = y}, Vector)
end
function Vector.__add(a, b)
return Vector.new(a.x + b.x, a.y + b.y)
end
function Vector.__mul(v, scalar)
return Vector.new(v.x * scalar, v.y * scalar)
end
function Vector.__unm(v)
return Vector.new(-v.x, -v.y)
end
function Vector.__eq(a, b)
return a.x == b.x and a.y == b.y
end
local v = Vector.new(1, 2) + Vector.new(3, 4)
print(v.x, v.y) -- 4, 6Arithmetic metamethods (__add, __sub, __mul, __div, __mod, __pow, __unm) allow objects to behave like native types. __eq, __lt, __le for comparisons. Essential for math types (Vector, Matrix, Complex) and transparent DSLs.
Encapsulation with Closures
-- Private state with closures:
local function new_account(initial_balance)
local balance = initial_balance -- private!
local history = {} -- private!
return {
deposit = function(v)
balance = balance + v
history[#history + 1] = "+" .. v
end,
withdraw = function(v)
if v > balance then
error("Insufficient balance")
end
balance = balance - v
end,
get_balance = function()
return balance
end,
}
end
local account = new_account(100)
account.deposit(50)
print(account.get_balance()) -- 150
-- account.balance -- nil! (inaccessible)Lua has no private/protected — encapsulation is achieved with closures: local variables are captured by the returned functions but inaccessible from outside. This "object the closure" pattern is safer than metatables (where fields are always accessible via rawget). Downside: more memory per object.
__tostring and __call
-- __tostring (used by print):
local Point = {}
Point.__index = Point
function Point.new(x, y)
return setmetatable({x = x, y = y}, Point)
end
function Point.__tostring(p)
return string.format("(%g, %g)", p.x, p.y)
end
local p = Point.new(3, 4)
print(p) -- "(3, 4)"
-- __call (object the function):
function Point.__call(p, factor)
return Point.new(p.x * factor, p.y * factor)
end
local p2 = p(2) -- called the a function!
print(p2) -- "(6, 8)"
-- __len (# operator):
function Point.__len(p)
return math.sqrt(p.x^2 + p.y^2)
end
print(#p) -- 5.0 (magnitude)__tostring is invoked by print() and tostring() — it allows readable object representation. __call allows using objects the functions (obj()) — useful for builders and DSLs. __len intercepts the # operator. __concat intercepts ... These metamethods make objects transparent.
__call the Constructor
-- Class the callable:
local Point = {}
Point.__index = Point
-- Metatable of AS CLASS (not the object!):
setmetatable(Point, {
__call = function(cls, x, y)
return setmetatable({x = x, y = y}, cls)
end
})
-- Point() works the constructor:
local p = Point(3, 4) -- no .new()!
print(p.x, p.y) -- 3, 4
-- With validation:
local Email = {}
Email.__index = Email
setmetatable(Email, {
__call = function(cls, addr)
assert(addr:match("@"), "Invalid email")
return setmetatable({addr = addr}, cls)
end
})
local e = Email("ana@mail.com") -- OK__call on the metatable of AS CLASS allows using the class the a function — Point(3, 4) instead of Point.new(3, 4). The first argument is the class itself (cls), followed by the args. It creates cleaner, more idiomatic APIs. It is the preferred pattern in modern Lua libraries. Combined with validation, the constructor becomes safe.