Lua Tutorial: Learn Embedded Language from Scratch (2026)
Lua is the language you find everywhere once you start looking: in game engines (World of Warcraft, Roblox), embedded systems (OpenWrt, Redis scripts), and configuration (Neovim, Awesome WM). Its design principle is simplicity — the entire reference manual fits in 100 pages. I have embedded Lua in C projects to provide scripting capabilities without integrating a heavy interpreter.
What makes Lua elegant is its consistent core: tables do everything. Arrays, dictionaries, objects, modules — all are tables. This uniformity makes the language easy to learn and surprisingly powerful.
Tables
Tables are Lua's universal data structure — they serve as arrays, dictionaries, sets, and objects. Table keys can be any value except nil. Array indices start at 1 by convention. The length operator # works on sequences (contiguous integer keys starting at 1). Tables are references: assignment and comparison work by reference, not value.
-- Table as array
local fruits = { "apple", "banana", "cherry" }
print(fruits[1]) -- apple (1-indexed)
print(#fruits) -- 3
-- Table as dictionary
local user = {
name = "Alice",
age = 30,
["email"] = "a@b.com"
}
-- Mixed
local data = {
"item",
key = "value",
[100] = "hundred"
}
Metatables
Metatables control table behavior through metamethods: __index for property lookup fallback, __newindex for property assignment, __add for operator overloading, __call to make a table callable, and __tostring for string representation. Metatables are the mechanism behind Lua's prototype-based OOP.
local Vector = {}
Vector.__index = Vector
function Vector.new(x, y)
return setmetatable({ x = x, y = y }, Vector)
end
function Vector:__add(other)
return Vector.new(self.x + other.x, self.y + other.y)
end
function Vector:__tostring()
return string.format("(%d, %d)", self.x, self.y)
end
local a = Vector.new(1, 2)
local b = Vector.new(3, 4)
print(a + b) -- (4, 6)
Coroutines
Coroutines provide cooperative multitasking. coroutine.create creates a coroutine from a function. coroutine.resume starts or continues execution; coroutine.yield suspends and returns control. Unlike threads, only one coroutine runs at a time — switching is explicit. This makes coroutines ideal for state machines, iterators, and cooperative scheduling.
function producer()
return coroutine.create(function()
for i = 1, 5 do
coroutine.yield(i * 10)
end
end)
end
local iter = producer()
-- Manual iteration
local status, val = coroutine.resume(iter)
while status and val do
print(val)
status, val = coroutine.resume(iter)
end
-- Wrapped as iterator
function range(n)
return coroutine.wrap(function()
for i = 1, n do coroutine.yield(i) end
end)
end
for i in range(5) do print(i) end
First-Class Functions
Functions are first-class values: you can store them in variables, pass them as arguments, and return them from other functions. Lua supports lexical scoping and closures. This enables higher-order functions, callbacks, and functional programming patterns despite the language's imperative core.
local function map(t, fn)
local result = {}
for i, v in ipairs(t) do
result[i] = fn(v)
end
return result
end
local function filter(t, pred)
local result = {}
for _, v in ipairs(t) do
if pred(v) then table.insert(result, v) end
end
return result
end
local nums = {1, 2, 3, 4, 5}
local evens = filter(nums, function(n) return n % 2 == 0 end)
local doubled = map(evens, function(n) return n * 2 end)
Module Pattern
Lua does not have a built-in module system — modules are tables. The standard pattern: a file returns a table of functions. require loads and caches modules, using the package path to find files. Modules can have local state hidden in closures. Lua 5.2+ added module() but the table-return pattern is preferred for clarity.
-- utils.lua
local M = {}
local function private_helper()
return "internal"
end
function M.public_hello(name)
return "Hello, " .. name
end
local function init()
-- module initialization
end
init()
return M
-- main.lua
local utils = require("utils")
print(utils.public_hello("Alice"))
Weak Tables
Weak tables allow garbage collection of keys and/or values. Use setmetatable(t, {__mode = "k"}) for weak keys, "v" for weak values, "kv" for both. Weak tables are essential for caching (memoization), associating data with objects without preventing their collection, and avoiding memory leaks in long-running scripts.
-- Cache with weak values: cached data
-- is collected when no other refs exist
local cache = setmetatable({}, { __mode = "v" })
function expensive_calc(key)
local cached = cache[key]
if cached then return cached end
local result = key * 1000 + math.random(1000)
cache[key] = result
return result
end
-- Weak keys: associate metadata with objects
local metadata = setmetatable({}, { __mode = "k" })
local obj = { name = "temp" }
metadata[obj] = { created_at = os.time() }
-- obj can be GC'd when last reference is gone
Frequently Asked Questions
Why is Lua 1-indexed?
Lua follows mathematical convention (matrices are 1-indexed) and the language designers believed it was more natural for non-programmers.
How does OOP work in Lua?
Through metatables and the colon syntax. Class-like behavior uses __index for method lookup and tables for instances.
What is the difference between ipairs and pairs?
ipairs iterates over integer keys starting at 1 until a nil. pairs iterates over all key-value pairs (unordered).
When to use coroutines vs callbacks?
Coroutines for sequential-looking async code and state machines. Callbacks for event-driven patterns with simple responses.
Originally published on Ayodhyyya. Last updated June 1, 2026.