Modules and packaging
A Lua module is a Lua source file (or a binary C library) returning a table of functions and values. The principal mechanism is require — admit loading, caching, and returning a module’s exports. The package.path and package.cpath define where modules are searched. LuaRocks is the conventional contemporary package manager for distributing Lua modules. The combination — file-as-module, require for loading with caching, the search-path mechanism, the LuaRocks ecosystem — is the substance of Lua’s package management.
A module
The conventional pattern: a module is a file returning a table:
-- File: greetings.lua
local M = {}
function M.hello(name)
return "Hello, " .. name
end
function M.goodbye(name)
return "Goodbye, " .. name
end
M.default_greeting = "Hi"
return M
Importing:
-- File: main.lua
local greetings = require("greetings")
print(greetings.hello("Alice")) -- "Hello, Alice"
print(greetings.default_greeting) -- "Hi"
require
The require mechanism:
- Checks
package.loaded[name]— returns cached if present. - Searches
package.pathfor.luafiles; triespackage.cpathfor.so/.dll. - Loads and runs the file (which returns the module table).
- Caches in
package.loaded[name]. - Returns the module.
The mechanism admits load-once semantics — substantial efficiency for repeated requires.
Module-private state
The conventional pattern uses local for module-private:
-- File: counter.lua
local M = {}
local count = 0 -- private to module
function M.increment()
count = count + 1
return count
end
function M.value()
return count
end
function M.reset()
count = 0
end
return M
local counter = require("counter")
counter.increment() -- 1
counter.increment() -- 2
print(counter.value()) -- 2
The module’s own local variables are not visible to callers — admit substantial encapsulation.
package.path
The search path for .lua files:
print(package.path)
-- "./?.lua;./?/init.lua;/usr/local/share/lua/5.4/?.lua;..."
The ? admits substitution of the module name. For example, require("foo") searches:
./foo.lua./foo/init.lua/usr/local/share/lua/5.4/foo.lua- …
For substantial customisation:
package.path = package.path .. ";./vendor/?.lua"
package.cpath
For C modules (.so on Linux, .dll on Windows):
print(package.cpath)
-- "./?.so;/usr/local/lib/lua/5.4/?.so;..."
local mylib = require("mylib") -- loads mylib.so
The C modules expose a luaopen_<name> function that registers Lua bindings.
Submodules
The . admits submodule paths:
-- File: mylib/utils.lua
local M = {}
function M.format(x)
return tostring(x)
end
return M
local utils = require("mylib.utils")
-- The . admits searching mylib/utils.lua
The require("a.b.c") searches for a/b/c.lua, a/b/c/init.lua, etc.
init.lua
A directory may admit a init.lua as the package’s main file:
mylib/
├── init.lua -- main entry
├── utils.lua -- mylib.utils
└── helpers.lua -- mylib.helpers
-- mylib/init.lua
local M = {}
M.utils = require("mylib.utils")
M.helpers = require("mylib.helpers")
function M.main()
-- ...
end
return M
local mylib = require("mylib") -- loads mylib/init.lua
mylib.utils.format(...)
mylib.main()
Module patterns
Standard table return
local M = {}
function M.foo() end
function M.bar() end
return M
Inline definitions
return {
foo = function() end,
bar = function() end
}
Function-style module (rare)
local M = {}
local function foo() end
local function bar() end
M.foo = foo
M.bar = bar
return M
Single-function module
local function process(input)
-- ...
end
return process
local process = require("processor")
process(input)
LuaRocks
LuaRocks is the conventional contemporary package manager:
luarocks install luasocket
luarocks install penlight
luarocks list -- installed
luarocks search keyword
luarocks remove package
A rockspec describes a package:
-- mylib-1.0-1.rockspec
package = "mylib"
version = "1.0-1"
source = {
url = "git+https://github.com/user/mylib"
}
description = {
summary = "A useful Lua library"
}
dependencies = {
"lua >= 5.1",
"luasocket >= 3.0"
}
build = {
type = "builtin",
modules = {
["mylib"] = "src/mylib.lua",
["mylib.utils"] = "src/mylib/utils.lua"
}
}
The conventional discipline:
- Develop in a project directory.
- Test locally with
luarocks make. - Publish via
luarocks upload.
Common patterns
Module with private state
-- File: cache.lua
local M = {}
local data = {}
function M.set(key, value)
data[key] = value
end
function M.get(key)
return data[key]
end
function M.clear()
data = {}
end
return M
Module with class
-- File: queue.lua
local Queue = {}
Queue.__index = Queue
function Queue.new()
return setmetatable({items = {}, head = 1, tail = 1}, Queue)
end
function Queue:push(item)
self.items[self.tail] = item
self.tail = self.tail + 1
end
function Queue:pop()
if self.head < self.tail then
local item = self.items[self.head]
self.items[self.head] = nil
self.head = self.head + 1
return item
end
end
function Queue:empty()
return self.head >= self.tail
end
return Queue
local Queue = require("queue")
local q = Queue.new()
q:push("a")
q:push("b")
print(q:pop()) -- "a"
Module returning a function
-- File: greet.lua
return function(name)
return "Hello, " .. name
end
local greet = require("greet")
print(greet("Alice")) -- "Hello, Alice"
Module with submodules
-- File: mymath/init.lua
local M = {}
M.basic = require("mymath.basic")
M.advanced = require("mymath.advanced")
return M
-- File: mymath/basic.lua
return {
add = function(a, b) return a + b end,
sub = function(a, b) return a - b end
}
local mymath = require("mymath")
print(mymath.basic.add(2, 3)) -- 5
Configuration loading
-- File: config.lua
local config_file = arg[1] or "config.lua"
local M = {}
local function load_config(path)
local fn, err = loadfile(path)
if not fn then return {}, err end
local sandbox = {}
debug.setupvalue(fn, 1, sandbox)
fn()
return sandbox
end
local config = load_config(config_file)
M.host = config.host or "localhost"
M.port = config.port or 8080
M.debug = config.debug or false
return M
Module as namespace
-- File: utils.lua
local M = {}
M.string = {
trim = function(s) return s:gsub("^%s*(.-)%s*$", "%1") end,
split = function(s, sep)
local parts = {}
for part in s:gmatch("[^" .. sep .. "]+") do
parts[#parts + 1] = part
end
return parts
end
}
M.table = {
deep_copy = function(t)
if type(t) ~= "table" then return t end
local result = {}
for k, v in pairs(t) do result[M.table.deep_copy(k)] = M.table.deep_copy(v) end
return result
end
}
return M
local utils = require("utils")
print(utils.string.trim(" hello ")) -- "hello"
Lazy loading via require cache
-- File: lazy.lua
return setmetatable({}, {
__index = function(_, key)
return require("lazy_implementations." .. key)
end
})
local lazy = require("lazy")
print(lazy.something) -- loads lazy_implementations.something
The mechanism admits substantial deferred loading.
Module override via package.loaded
-- For testing — replace a module:
package.loaded["http_client"] = mock_http_client
-- Then code that calls require("http_client") gets the mock
local client = require("http_client")
Forwarding require
-- File: shorthand.lua
return {
api = require("very.long.package.name.api"),
utils = require("very.long.package.name.utils")
}
require with relative paths (5.4+)
The require admits relative module paths in Lua 5.4 (limited support):
-- Within a package:
local utils = require("..utils") -- sibling module
The mechanism is implementation-specific; the conventional discipline uses absolute module names.
Reloading modules
-- Force reload (useful in REPL):
package.loaded["mymodule"] = nil
local mymodule = require("mymodule")
The package.loaded cache admits explicit invalidation; conventional in development workflows.
Plugin pattern
-- File: plugins/init.lua
local M = {}
M.plugins = {}
function M.register(plugin)
M.plugins[plugin.name] = plugin
if plugin.on_load then plugin.on_load() end
end
function M.run(event, ...)
for _, plugin in pairs(M.plugins) do
if plugin[event] then plugin[event](...) end
end
end
return M
-- File: plugins/logger.lua
local plugins = require("plugins")
plugins.register({
name = "logger",
on_request = function(req) print("request:", req) end,
on_response = function(resp) print("response:", resp) end
})
Module with __index for missing attributes
local M = setmetatable({}, {
__index = function(_, key)
error("attempt to access undefined module member: " .. key, 2)
end
})
function M.foo() return "foo" end
function M.bar() return "bar" end
return M
Versioned API
-- File: mylib/init.lua
local M = {}
M._VERSION = "1.0.0"
function M.version_info()
return {
version = M._VERSION,
lua = _VERSION
}
end
return M
The _VERSION is the conventional Lua module version field.
Cross-platform module
-- File: platform.lua
local M = {}
local function detect()
if package.config:sub(1, 1) == "\\" then
return "windows"
else
return "unix"
end
end
M.os = detect()
M.path_sep = (M.os == "windows") and "\\" or "/"
M.line_sep = (M.os == "windows") and "\r\n" or "\n"
return M
The package.config admits substantial platform detection.
A note on module()
Lua 5.1 had a module() function for declaring modules; removed in 5.2+:
-- Lua 5.1 only:
module("mymodule", package.seeall)
function foo() end -- becomes mymodule.foo
-- Modern equivalent (5.2+):
local M = {}
function M.foo() end
return M
The conventional contemporary discipline always uses the local M = {}; ... return M pattern — admit substantial portability.
A note on the conventional discipline
The contemporary Lua modules-and-packaging advice:
- Use the
local M = {}; ... return Mpattern. - Use
localfor module-private. - Use
requirefor loading; admit substantial caching. - Use submodules via dot-paths (
mylib.utils). - Use
init.luafor package main files. - Use LuaRocks for distribution.
- Cache hot modules as locals:
local string_format = string.format. - Use
_VERSIONfield for version info. - Avoid
module()— removed in 5.2+. - Test with
package.loadedoverrides.
The combination — file-as-module returning a table, require for loading with caching, the package.path/package.cpath search mechanism, the LuaRocks ecosystem, the conventional local M = {}; ... return M pattern — is the substance of Lua’s package management. The discipline produces clear, well-encapsulated, distributable code with substantial flexibility through the small core mechanism.