Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create suite of interdependent Lua files without affecting the global namespace

Tags:

lua

tl;dr: What design pattern allows you to split Lua code over multiple files that need to share some information without affecting the global table?

Background

It is considered bad form to create a library in Lua where requiring the library affects the global namespace:

--> somelib.lua <--
SomeLib = { ... }

--> usercode.lua <--
require 'somelib'
print(SomeLib) -- global key created == bad

Instead, it is considered a best practice to create a library that uses local variables and then returns them for the user to assign as they see fit:

--> somelib.lua <--
local SomeLib = { ... }
return SomeLib

--> usercode.lua <--
local theLib = require 'somelib' -- consumers name lib as they wish == good

The above pattern works fine when using a single file. However, this becomes considerably harder when you have multiple files that reference each other.

Concrete Example

How can you rewrite the following suite of files so that the assertions all pass? Ideally the rewrites will leave the same files on disk and responsibilities for each file. (Rewriting by merging all code into a single file is effective, but not helpful ;)

--> test_usage.lua <--
require 'master'

assert(MASTER.Simple)
assert(MASTER.simple)
assert(MASTER.Shared)
assert(MASTER.Shared.go1)
assert(MASTER.Shared.go2)
assert(MASTER.Simple.ref1()==MASTER.Multi1)
assert(pcall(MASTER.Simple.ref2))
assert(_G.MASTER == nil)                   -- Does not currently pass 

 

--> master.lua <--
MASTER = {}
require 'simple'
require 'multi'
require 'shared1'
require 'shared2'
require 'shared3'
require 'reference'

--> simple.lua <--
MASTER.Simple = {}
function MASTER:simple() end

--> multi.lua <--
MASTER.Multi1 = {}
MASTER.Multi2 = {}

--> shared1.lua <--
MASTER.Shared = {}

--> shared2.lua <--
function MASTER.Shared:go1() end

--> shared3.lua <--
function MASTER.Shared:go2() end

--> reference.lua <--
function MASTER.Simple:ref1() return MASTER.Multi1 end
function MASTER.Simple:ref2() MASTER:simple()      end

Failure: Setting the Environment

I thought to solve the problem by setting the environment to my master table with a self-reference. This does not work when calling functions like require however, as they change the environment back:

--> master.lua <--
foo = "original"
local MASTER = setmetatable({foo="captured"},{__index=_G})
MASTER.MASTER = MASTER
setfenv(1,MASTER)
require 'simple'

--> simple.lua <--
print(foo)         --> "original"
MASTER.Simple = {} --> attempt to index global 'MASTER' (a nil value)
like image 786
Phrogz Avatar asked Feb 18 '13 17:02

Phrogz


2 Answers

You are giving master.lua two responsibilities:

  1. It defines the common module table
  2. It imports all of the submodules

Instead you should create a separate module for (1) and import it in all of the submodules:

--> common.lua <--
return {}

--> master.lua <--
require 'simple'
require 'multi'
require 'shared1'
require 'shared2'
require 'shared3'
require 'reference'
return require'common' -- return the common table

--> simple.lua <--
local MASTER = require'common' -- import the common table
MASTER.Simple = {}
function MASTER:simple() end

etc.

Finally, change the first line of test_usage.lua to use a local variable:

--> test_usage.lua <--
local MASTER = require'master'
...

The tests should now pass.

like image 56
Simon C. Avatar answered Nov 15 '22 12:11

Simon C.


I have a systematic way to solve that problem. I have refactored your module in a Git repository to show you how it works: https://github.com/catwell/dont-touch-global-namespace/commit/34b390fa34931464c1dc6f32a26dc4b27d5ebd69

The idea is that you should have the sub-parts return a function that takes the main module as an argument.

If you cheat by opening the source files in master.lua, append a header and a footer and use loadstring, you can even use them unmodified (only master.lua has to be modified, but it is more complex). Personally, I prefer to keep it explicit, which is what I have done here. I don't like magic :)

EDIT: it is very close to Andrew Stark's first solution, except I patch the MASTER table directly in the sub-modules. The advantage is that you can define several things at once, like in your simple.lua, multi.lua and reference.lua files.

like image 40
catwell Avatar answered Nov 15 '22 11:11

catwell