=== Using the module Function === module(..., package.seeall) local x = 1 local function baz() print 'test' end function foo() print("foo", x) end function bar() foo() baz() print "bar" end require 'mymodule' mymodule.bar()
This is also common and shorter. It uses Lua's module function. Some other ways of using the module function are in Programming in Lua [1]. However, see LuaModuleFunctionCritiqued for criticisms of this approach. === From a Table - Using Locals Internally === local M = {} local x = 1 local function baz() print 'test' end local function foo() print("foo", x) end M.foo = foo local function bar() foo() baz() print "bar" end M.bar = bar return M
This is similar to the table approach, but even inside the module itself it uses lexicals when referring to externally faced variables. Although this code is more verbose (repetitive), lexicals can be more efficient for performance critical code and more suitable for static analysis approaches to DetectingUndefinedVariables. Futhermore, this approach prevents changes made to M--e.g. from a client--from affecting the behavior inside the module; for example, in the regular tables approach, M.bar() internally calls M.foo(), so the behavior of M.bar() will change if M.foo() were replaced. This has some implications for SandBoxes too, and it is the reason for the extra locals in etc/strict.lua in Lua 5.1.3. === localmodule === local M = {} local x = 1 local M_baz = 1 local function M_foo() M_baz = M_baz + 1 print ("foo", x, M_baz) end local function M_bar() M_foo() print "bar" end require 'localmodule'.export(M) return M local MM = require 'mymodule' MM.baz = 10 MM.bar() MM.foo = function() print 'hello' end MM.bar()
This approach is more novel. It defines all externally facing variables in the module using lexical (local) variables. It makes heavy use of lexicals. Reliance on lexicals has some advantages such as when using the static analysis methods of DetectingUndefinedVariables. The export function uses the debug module to read the current function's local variables prefixed by M_ (debug.getlocal) and expose them (read/write) through the module table M via metafunctions. The ability to write to these variables is made possible by searching for and using (whenever possible) the upvalues located in closures (debug.getupvalue/debug.getupvalue), such as in the nested closures. This avoids the repetition seen in "From a Table - Using Locals Internally". You may selectively replace M_foo style references with M.foo style references if more dynamic behavior is desired. The implementation of localmodule assumes that symbols have not been stripped (luac -s) and that the debug module has not been removed, so this approach does have a bit more baggage. The localmodule module is defined as local M = {} local getupvalue = debug.getupvalue local setupvalue = debug.setupvalue local function makemt(t) local mt = getmetatable(t) if not mt then mt = {} setmetatable(t, mt) end local varsf,varsi = {},{} function mt.__index(_,k) local a = varsf[k] if a then local _,val = getupvalue(a,varsi[k]) return val end end function mt.__newindex(_,k,v) local a = varsf[k] if a then setupvalue(a,varsi[k], v) end end return varsf,varsi end local function export(P) P = P or {} local varsf,varsi = makemt(P) for i=1,math.huge do local name,val = debug.getlocal(2, i) if val == nil then break end if type(val) == 'function' then local f = val for j=1,math.huge do local name,val = debug.getupvalue(f, j) if val == nil then break end if name:find("M_") == 1 then name = name:sub(3) varsf[name] = f varsi[name] = j end end end end for i=1,math.huge do local name,val = debug.getlocal(2, i) if val == nil then break end if name:find("M_") == 1 then name = name:sub(3) if not varsf[name] then rawset(P, name, val) end end end return P end M.export = export return M
=== Pattern: Module System with Public/Private Namespaces === As noted in Programming in Lua, 2nd edition p.144, when using the Lua 5.1 module system with the package.seeall option (or the equivalent setmetatable(M, {__index = _G}) trick), there is a peculiarity in that global variables are accessible through the module table. For example, if you have a module named complex defined as such: module("complex", package.seeall)
then doing require "complex" print(complex.math.sqrt(2))
prints the square root of 2 because math is a global variable. Furthermore, if a global variable with name complex already exists (possibly defined in some unrelated file), then the require will fail: -- put this in the main program: complex = 123 -- then deep in some module do this: local c = require "complex" --> fails with "name conflict for module 'complex'"
This is a type of namespace pollution and possibly a source of errors. The problem as I see it is that the environment used internally by the module is the same as the table exposed to the client of the module. We can make these two separate tables as given in the below solution: function cleanmodule(modname) local pub = {} local priv = {} local privmt = {} privmt.__index = _G privmt.__newindex = function(priv, k, v) rawset(pub, k, v) rawset(priv, k, v) end setmetatable(priv, privmt) setfenv(2, priv) package.loaded[modname] = pub end function cleanrequire(name) local result = require(name) rawset(getfenv(2), name, result) return result end
Example usage: require "cleanmodule" m2 = 123 cleanrequire "m1" m1.test() assert(m1) assert(not m1.m2) assert(m1.test) assert(m1.helper) assert(m2 == 123) print("done")
cleanmodule(...) cleanrequire "m2" function helper() print("123") end function test() helper() m2.test2() end assert(not m1) assert(test) assert(helper) assert(m2) assert(m2.test2) assert(not m2.m1) assert(not m2.m2)
cleanmodule(...) function test2() print(234) end
Output: 123 234 done
Take #2 - Here is the latest refinement of the previous code. This version only replaces module not require. local function import(public, ...) local target, options = ... if type(target) ~= "table" then target, options = nil, target end target = target or getfenv(2) if options == ":all" then for k,v in pairs(public) do target[k] = v end end local prevtable, prevprevtable, prevatom = target, nil, nil public._NAME:gsub("[^%.]+", function(atom) local table = rawget(prevtable, atom) if table == nil then table = {}; rawset(prevtable, atom, table) elseif type(table) ~= 'table' then error('name conflict for module ' .. public._NAME, 4) end prevatom = atom; prevprevtable = prevtable; prevtable = table end) rawset(prevprevtable, prevatom, public) return public end function cleanmodule(modname) local pubmt = {__call = import} local pub = {import = import, _NAME = modname} local priv = {_PUBLIC = pub, _PRIVATE = priv, _NAME = modname} local privmt = { __index = _G, __newindex = function(priv, k, v) rawset(pub, k, v) rawset(priv, k, v) end } setmetatable(pub, pubmt) setmetatable(priv, privmt) setfenv(2, priv) pub:import(priv) package.loaded[modname] = pub end
This is typically used in this way: require "cleanmodule" cleanmodule(...) local om = require "othermodule" om.hello() require "othermodule" () othermodule.hello() require "othermodule" ":all" hello()
The caller has full control in deciding how it wants to the called module to modify the caller's (private) namespace. One small problem you might run into is when setting a global twice: cleanmodule(...) local enable_spanish = true function test() print("hello") end if enable_spanish then test = function() print("hola") end end
Here, the metamethod only activates on the first set, so the public namespace will incorrectly contain the first function defined above. The work around is to explicitly set to nil: cleanmodule(...) local enable_spanish = true function test() print("hello") end if enable_spanish then test = nil; test = function() print("hola") end end
(This example was originally in LuaDesignPatterns.) --DavidManura, 200703 Take #3 - Here is some further refinement of Take #2. This is a trivial change but might be useful. I placed the cleanmodule code in an anonymous function and called the anonymous function. I also included _G in the private module table. This code can be placed at the beginning of any module file and will not replace any functions at all. It has the same problem as Take #2 when replacing a value but the same workaround will work. (function (modname) local function import(public, ...) local target, options = ... if type(target) ~= "table" then target, options = nil, target end target = target or getfenv(2) if options == ":all" then for k,v in pairs(public) do target[k] = v end end local prevtable, prevprevtable, prevatom = target, nil, nil public._NAME:gsub("[^%.]+", function(atom) local table = rawget(prevtable, atom) if table == nil then table = {}; rawset(prevtable, atom, table) elseif type(table) ~= 'table' then error('name conflict for module ' .. public._NAME, 4) end prevatom = atom; prevprevtable = prevtable; prevtable = table end) rawset(prevprevtable, prevatom, public) return public end local pubmt = {__call = import} local pub = {import = import, _NAME = modname} local priv = {_PUBLIC = pub, _PRIVATE = priv, _NAME = modname, _G = _G } local privmt = { __index = _G, __newindex = function(priv, k, v) rawset(pub, k, v) rawset(priv, k, v) end } setmetatable(pub, pubmt) setmetatable(priv, privmt) setfenv(2, priv) pub:import(priv) package.loaded[modname] = pub end)(...)
--PeterSchwier?, 2009Feb04 Take #4 - Here a rework of Take #2. This achieves public/private namespaces within the existing framework of the module function (without replacing module nor require). However, it does nothing to address the problem of the module function writing to _G rather than to the client's private environment (which might be thought of as an orthogonal problem solvable by redefining module). function package.clean(module) local privenv = {_PACKAGE_CLEAN = true} setfenv(3, setmetatable(privenv, {__index=_G, __newindex=function(_,k,v) rawset(privenv,k,v); module[k]=v end} )) end return package.clean
function package.veryclean(module) local privenv = {M=module, _PACKAGE_VERYCLEAN = true} setfenv(3, setmetatable(privenv, {__index=_G})) end return package.veryclean
function package.strict(t) local privenv = getfenv(3) local top = debug.getinfo(3,'f').func local mt = getmetatable(privenv) function mt.__index(t,k) local v=_G[k] if v ~= nil then return v end error("variable '" .. k .. "' is not declared", 2) end if rawget(privenv, '_PACKAGE_CLEAN') then local old_newindex = assert(mt.__newindex) function mt.__newindex(t,k,v) if debug.getinfo(2,'f').func ~= top then error("assign to undeclared variable '" .. k .. "'", 2) end old_newindex(t,k,v) end else function mt.__newindex(t,k,v) error("assign to undeclared variable '" .. k .. "'", 2) old_newindex(t,k,v) end end end return package.strict
Take #5 - a rework of Take #4 on package.clean(). Uses proxy tables to solve the redeclaration problem. Inherits CLEAN_ENV instead of _G to avoid seeing the polluted global environment, thus solving the problem of dependencies hiding that module() introduces. You could copy the contents of _G into CLEAN_ENV at the very beginning of your program for instance so that modules always see a Lua environment clean of any externally introduced dependencies. local CLEAN_ENV = { pairs = pairs, unpack = unpack, ... } local P_meta = {__index = CLEAN_ENV} function package.clean(M) local P = setmetatable({}, P_meta) setfenv(3, setmetatable({}, {__index = P, __newindex = function(t,k,v) M[k]=v; P[k]=v; end})) end
--CosminApreutesei, 2009oct Take #6 an adaption of Take #1 -- the first example, with inspiration from #4 to split module and seeall into orthogonal functions. Here, we use one single table for the module namespace, to avoid all sync issues with the double system. The private module environment is an empty proxy table, with a custom-defined lookup routine (_M[k] or _G[k], that's it). The indirections in private lookups assume that module lookups are more important to be fast externally than internally (you can use locals internally). local function _module(modname, ...) local _M = {} setfenv(2, _M) _M._M = _M _M._NAME = modname if ... then for _, func in ipairs({...}) do func(_M) end end package.loaded[modname] = _M end local function _seeall(_M) local priv = {} local privmt = {} privmt.__index = function(priv, k) return _M[k] or _G[k] end privmt.__newindex = _M setmetatable(priv, privmt) setfenv(3, priv) end local g_require = require local function _require(name) local result = g_require(name) rawset(getfenv(2), name, result) return result end module(...) module = _module seeall = _seeall require = _require
-- Ulrik, 2010apr Take #7 possible module declaration for Lua 5.2 function module(...) local m={} for k,v in ipairs{...} do if type(v)=="table" then setmetatable(m,{__index=v}) elseif type(v)=="function" then v(m) elseif type(v)=="string" then m.notes=v end end return m end function makeenv(list,r0) local r={} for i in string.gmatch(list,"%a+") do r[i]=_G[i] end for k,v in pairs(r0) do r[k]=v end return r end function safeenv(m) return makeenv([[getmetatable assert pcall select type rawlen rawequal rawset rawget tonumber next tostring xpcall error ipairs unpack setmetatable pairs string,math,table,coroutine,bit32,_VERSION]],m) end function stdenv(m) m=safeenv(m) m=makeenv([[print loadfile require load loadstring dofile collectgarbage os io package debug]],m) return m end return module("my mega module",safeenv{trace=print},function(_ENV) a=20 local b=30 function dump(x) for k,v in pairs(x) do trace(k,v) end end local function do_something() a=a+1 end end) return module("some description",_G,function(_ENV) public_var=12345 local private_var=54321 public_fn=print local private_fn=print end) local m1=require "module1" m1.dump(m1)
|