Skip to content
Lesson 17 of 18

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.lua to 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 — require 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 filename — require("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.

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.

Up nextCapstone: a word indexFunctions

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…