Step 1 of 5 · Reading · ~4 min
Learn
Functions
Modules: local tables, return, require
Every program in this course has been a single file. Real Lua never is: a Neovim plugin or an OpenResty handler is a pile of files that load one another. The mechanism is smaller than you would guess. A module is a table, and loading one is a function call.
The shape
A module file builds a local table, hangs functions off it, and returns it as the last statement of the file.
-- textutil.lua
local M = {}
function M.trim(s)
return (s:gsub("^%s+", ""):gsub("%s+$", ""))
end
function M.words(s)
local out = {}
for w in s:gmatch("%S+") do out[#out + 1] = w end
return out
end
return M
No new machinery. M is an ordinary table, M.trim an ordinary field holding an ordinary function, and return works at the top of a file because a Lua file is a function — the manual calls it a chunk. The parentheses around the trim body matter for the reason String Patterns gave: gsub returns two values, and without them the caller would get the substitution count too.
local is the whole privacy story
Leave local off and the name becomes a global — reachable from, and overwritable by, every other file in the process.
helper = function() return 1 end -- global: lands in _G
local function helper2() return 2 end -- local: stays in this file
print(type(_G.helper), type(_G.helper2))
--> function nil
A helper you never meant to publish stays invisible until a second module picks the same name. Declare everything local, and publish deliberately by putting it on M.
require
Another file then loads the module by name:
local textutil = require("textutil")
print(textutil.trim(" hi "))
require searches package.path for textutil.lua, runs it once, and returns whatever the chunk returned. Ask again and the file is not re-read: the result is cached in package.loaded, so every file that requires a module shares one copy of it.
Grader note: those two lines cannot run here. This editor submits one file, so there is no
textutil.luato find, and the submission dies before printing anything.
Output (the real search list is longer):
/usr/local/lua-5.3.5/lua53: script.lua:1: module 'textutil' not found:
no field package.preload['textutil']
no file '/usr/local/share/lua/5.3/textutil.lua'
no file './textutil.lua'
no file './textutil.so'
Everything else on this page works in one file. Write your modules as local tables now; require is the line you add the day the project grows a second file.
Watching require work anyway
Read the first line of that error again: no field package.preload['textutil']. Before it touches the filesystem, require consults a table of loader functions — and you may put one there yourself, which is enough to watch the real contract without a second file.
local loads = 0
package.preload["greet"] = function()
loads = loads + 1
return {hi = function(name) return "hi " .. name end}
end
local g = require("greet")
require("greet")
require("greet")
print(g.hi("ada"), loads, package.loaded["greet"] == g)
--> hi ada 1 true
Three calls, one load, and package.loaded holding the very table the loader returned. That caching is the point of require: whatever a module does at load time — read a config file, build a lookup table — happens exactly once, however many files ask for it.
Common mistakes
- Forgetting the final
return—requirestorestrueinstead of your table, and the caller dies onattempt to index a boolean value. - Leaving off
local— every helper becomes a global, and two modules that each definesplitoverwrite one another in silence. - Requiring a filename —
require("textutil.lua")searches fortextutil/lua.lua, because a dot in a module name is a directory separator. - Expecting
requireto re-read the file — it caches, so a module's top-level code runs once per process and never again.
Your exercise
Text Kit builds a module in one file and then uses it as data. You write three functions on a local table textkit, and the driver looks each command up in that table with textkit[op] — possible only because a module is an ordinary table.
Two things the grader will catch. The functions must return, not print: the driver does the printing, so a textkit.words that calls print itself emits the count and then a blank line from the driver's own print. And the closing exports: line lists the keys alphabetically — they come out of pairs, whose order is unspecified, so collect them into a list and table.sort it before joining.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…