Lua does a lot of work with globals. I suspect some of this comes down to performance (it’s more efficient to tweak global state than copy values in and out of function calls) some to the language’s obsession with minimalism (which also might come down to performance) and its nature as a hosted language.
Regardless, if I had a bunch of utility functions for manipulating rectangles, the most natural way to do this in Lua would probably be to define the all at the top level of my program. In LÖVE, that means in main.lua
:
-- main.lua
function expand(by, x, y, width, height)
return x-by, y-by, width+(by*2), height+(by*2)
end
function contract(by, x, y, width, height)
return expand(-by, x, y, width, height)
end
function love.draw()
local screen = {0,0,400,240}
love.graphics.setColor({0,0,0})
love.graphics.rectangle(
"fill",
contract(20, unpack(screen))
)
end
In a way, this all feels very “capital-F” Functional, and a great many are happy with this solution and should stop reading here. But it drives me nuts for a couple of reasons:
- Being global, these functions are polluting my namespace. What if I wanted to have a variable to represent a legal document my character signed. I couldn’t call it
contract
without shadowing my global function — a recipe for future heisenbugs. - I’m loading code I may not use. Sure, insetting a rectangle by a given amount seems useful now, but what if requirements change and I don’t need it anymore. Will I ever audit my gigantic list of utility functions and prune the dead code? Will it just take up RAM and bloat my footprint? 1
We can address the first problem pretty easily. Just create an empty table and assign our functions to that:2
-- main.lua
Util = {}
Util.rect = {}
function Util.rect.expand(by, x, y, width, height)
return x-by, y-by, width+(by*2), height+(by*2)
end
function Util.rect.contract(by, x, y, width, height)
return expand(-by, x, y, width, height)
end
function love.draw()
local screen = {0,0,400,240}
love.graphics.setColor({0,0,0})
love.graphics.rectangle(
"fill",
Util.rect.contract(20, unpack(screen))
)
end
But how do we make sure we only load code we need?
So far as I know, there’s no sort of dynamic “autoload” linking or anything in Lua. But it does have the ability to pars and eval other Lua files on demand via the built-in dofile
function. So we could imagine putting all our rectangle-related utilities in a file called rect.lua
that just returned a table mapping names to functions:
-- rect.lua
return {
expand = function(by, x, y, width, height)
return x-by, y-by, width+(by*2), height+(by*2)
end,
contract = function(by, x, y, width, height)
return x+by, y+by, width-(by*2), height-(by*2)
end
}
We could then load it in main like so:
-- main.lua
Util = {}
Util.rect = dofile("rect.lua")
Which helps us keep things organized… but still doesn’t fix “load it if we need it” problem. We’re still loading this globally in main every time the program is run.
We could instead load it into a local variable of the files that need it:
-- myFileUsingRects.lua
local MyRectUtils = doFile("rect.lua")
function insetFromScreen()
local screen = {0,0,400,240}
return MyRectUtils.contract(10, unpack(screen))
end
But if more than one file uses rect.lua
we’ve actually compounded the problem, loading multiple copies of these functions into RAM.3
To solve this, we’re going to take a tip from C’s preprocessor and do the simplest thing that can work:
-- main.lua
Util = {}
function Util.import(name)
if Util[name] == nil then
Util[name] = dofile(name)
end
end
-- myFileUsingRects.lua
Util.import "rect"
function insetFromScreen()
local screen = {0,0,400,240}
return Util.rect.contract(10, unpack(screen))
end
This is a good tradeoff between concerns, IMHO. A file is never loaded unless explicitly imported, and even if imported many times, it’s only ever loaded once.
While we’re adding base functions to our Util
namespace, here’s a little modification I’ve found usefule:
-- main.lua
function Util.define(f)
local t = {}
f(t)
return t
end
This lets us define our utilities by modifying a table passed to a function rather than returning the table itself. This way we have access to anything defined in the function — local variables and functions that we can use “private” helper methods, for example. We also have access to other “public” values we’ve already defined — letting us go back to defining contract
in terms of expand
:
-- rect.lua
return Util.define{function(export)
local function secret()
print("only callable within this file")
end
function export.expand(by, x, y, width, height)
return x-by, y-by, width+(by*2), height+(by*2)
end
function export.contract(by, x, y, width, height)
return export.expand(-by, x, y, width, height)
end
end}
Next time we’ll see how we can also use this mechanism to pass initialization parameters to our module. Very useful when using this strategy to create stand-alone objects like views…
1: I don’t actually know how Lua handles function lookup. But given its minimalism and reliance on tables, I assume there’s a “global table” that holds every global variable or function we define. So even unused functions, if assigned to this table, would take up space. ↩︎
2: I’ve seen a lot of disagreement as to whether global namespaces like this ought to be capitalized or not. Lua namespaces like table
are not. And LÖVE’s love
namespace is not. But many linters warn of all-lower-case globals.
In most other languages I’m familiar with, these would be capitalized. And it feels like there’s semantic value in distinguishing global containers like this. So I’m going with capitalized. ↩︎
3: Assuming Lua doesn’t do any sort of caching in this regard. ↩︎