Skip to content
Modules: local tables, return, require
step 1/5

Reading — step 1 of 5

Learn

~4 min readFunctions

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.lua to find, and the submission dies before printing anything.

Output — the search list is twelve paths, trimmed to four here:

/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 returnrequire stores true instead of your table, and the caller dies on attempt to index a boolean value.
  • Leaving off local — every helper becomes a global, and two modules that each define split overwrite one another in silence.
  • Requiring a filenamerequire("textutil.lua") searches for textutil/lua.lua, because a dot in a module name is a directory separator.
  • Expecting require to re-read the file — it caches, so a module's top-level code runs once per process and never again.
  • Exporting by assigning to _G — the returned table is the export list; anything off it should stay unreachable.

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…