替代模組定義 |
|
module
函數
module(..., package.seeall) -- optionally omitting package.seeall if desired -- private local x = 1 local function baz() print 'test' end function foo() print("foo", x) end function bar() foo() baz() print "bar" end -- Example usage: require 'mymodule' mymodule.bar()
這也廣為人知,且較簡潔。它使用 Lua 的 module
函數。使用 module
函數的其他一些方法在 用 Lua 程式設計 [2] 有提到。然而,請參閱 LuaModuleFunctionCritiqued 以了解此方法的批判。
local M = {} -- private 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
這類似於表格方法,但即使在模組本身內部,它也是在參考外部面向變數時使用文字。雖然此程式碼較 verbose(重複的),但文字對於效能關鍵的程式碼來說可能更有效率,而且更適合於 DetectingUndefinedVariables 的靜態分析方法。此外,此方法可防止對 M
所做的變更(例如,來自客戶端)影響模組內的行為;例如,在常規表格方法中,M.bar()
在內部呼叫 M.foo()
,因此如果 M.foo()
被取代,則 M.bar()
的行為將會改變。這對於 SandBoxes 也有一些影響,而且這是 Lua 5.1.3 中 etc/strict.lua
中額外部分區域變數的原因。
local M = {} local x = 1 -- private local M_baz = 1 -- public 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 -- Example usage: local MM = require 'mymodule' MM.baz = 10 MM.bar() MM.foo = function() print 'hello' end MM.bar() -- Output: -- foo 1 11 -- bar -- hello -- bar
此方法較新穎。它使用文字(區域變數)變數定義模組中所有外部面向變數。它大量使用文字。依賴文字有一些優點,例如在使用 DetectingUndefinedVariables 的靜態分析方法時。
export
函數使用 debug
模組讀取由 M_
開頭的目前函數的區域變數(debug.getlocal
)並透過元函數讓它們能透過模組表格 M
公開(讀取/寫入)。寫入這些變數的能力,是透過尋找並使用(在可能的情況下)位於封閉(debug.getupvalue/debug.getupvalue
)中的 upvalue,例如在巢狀封閉中,得以達成。這避免了在「從表格 - 內部使用區域變數」中看到的重複。如果您希望獲得更動態的行為,可以選擇性地使用 M.foo
樣式的參考取代 M_foo
樣式的參考。
localmodule
的實作假設符號沒有被刪除(luac -s),而且 debug 模組沒有被移除,因此此方法的確有些缺點。
localmodule
模組被定義為
-- localmodule.lua -- David Manura, 2008-03, Licensed under the same terms as Lua itself (MIT License). local M = {} -- Creates metatable. 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 -- Makes locals in caller accessible via the table P. local function export(P) P = P or {} local varsf,varsi = makemt(P) -- For each local variable, attempt to locate an upvalue -- for it in one of the local functions. -- -- TODO: This may have corner cases. For example, we might want to -- check that these functions are lexically nested in the current -- function (possibly with something like lbci). 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 --print('DEBUG:upvalue', name) end end end end -- For each local variable, it no upvalue was found, just -- resort to making a copy of it instead. 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) --print('DEBUG:copy', name) end end end return P end M.export = export return M
如同 Programming in Lua 第 2 版的第 144 頁所述,在以 package.seeall
選項(或等值的 setmetatable(M, {__index = _G})
技巧)使用 Lua 5.1 模組系統時,有一個特點是全域變數可透過模組表格取得。例如,如果您有一個名為 complex
的模組定義如下
-- complex.lua module("complex", package.seeall) -- ...
然後執行
require "complex" print(complex.math.sqrt(2))
會列印出 2 的平方根,因為 math
是全域變數。此外,如果已存在一個名稱為 complex
的全域變數(可能在某個不相關的檔案中定義),則 require
會失敗
-- 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'"
這是一種名稱空間污染,可能造成錯誤。
在我看來,問題在於模組內部使用的環境與提供給模組客戶端的表格相同。我們可以將這兩個表格分開,如下面的解決方案所述。
-- cleanmodule.lua -- Declare module cleanly. -- Create both public and private namespaces for module. -- Global assignments inside module get placed in both -- public and private namespaces. function cleanmodule(modname) local pub = {} -- public namespace for module local priv = {} -- private namespace for module local privmt = {} privmt.__index = _G privmt.__newindex = function(priv, k, v) --print("DEBUG:add",k,v) rawset(pub, k, v) rawset(priv, k, v) end setmetatable(priv, privmt) setfenv(2, priv) package.loaded[modname] = pub end -- Require module, but store module only in -- private namespace of caller (not public namespace). function cleanrequire(name) local result = require(name) rawset(getfenv(2), name, result) return result end
範例用法
-- test.lua require "cleanmodule" m2 = 123 -- variable that happens to have same name as a module cleanrequire "m1" m1.test() assert(m1) assert(not m1.m2) -- works correctly! assert(m1.test) assert(m1.helper) assert(m2 == 123) -- works correctly! print("done")
-- m1.lua 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)
-- m2.lua cleanmodule(...) function test2() print(234) end
輸出
123 234 done
使用範例 2 - 這是先前程式碼的最新改版。此版本只取代 module
,不取代 require
。
-- cleanmodule.lua -- Helper function added to modules defined by cleanmodule -- to support importing module symbols into client namespace. -- Usage: -- local mm = require "mymodule" -- only local exported -- require "mymodule" () -- export module table to environment -- require "mymodule" ":all" -- export also all functions -- to environment. -- require "mymodule" (target,":all") -- export instead to given table local function import(public, ...) -- Extract arguments. local target, options = ... if type(target) ~= "table" then target, options = nil, target end target = target or getfenv(2) -- Export symbols. if options == ":all" then for k,v in pairs(public) do target[k] = v end end -- Build public module tables in caller. 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 -- Declare module cleanly. -- Create both public and private namespaces for module. -- Global assignments inside module get placed in both -- public and private namespaces. function cleanmodule(modname) local pubmt = {__call = import} local pub = {import = import, _NAME = modname} -- public namespace for module local priv = {_PUBLIC = pub, _PRIVATE = priv, _NAME = modname} -- private namespace for module 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
一般以這種方式使用
-- somemodule.lua require "cleanmodule" cleanmodule(...) local om = require "othermodule" om.hello() require "othermodule" () othermodule.hello() require "othermodule" ":all" hello()
呼叫者有完全的控制權,決定它要如何讓所呼叫的模組變更呼叫者的 (私有) 名稱空間。
一個可能遇到的問題,是兩次設定全域變數時
cleanmodule(...) local enable_spanish = true function test() print("hello") end if enable_spanish then test = function() print("hola") end end
這裡,元方法只在第一次設定時啟用,因此公開名稱空間會錯誤地包含以上定義的第一個函式。解決方法是將明確定義設為 `nil`。
cleanmodule(...) local enable_spanish = true function test() print("hello") end if enable_spanish then test = nil; test = function() print("hola") end end
(此範例最初出現在 Lua Design
Patterns.)
--DavidManura,200703
使用範例 3 - 這是使用範例 2 的後續改版。這是一個細微變更,但可能會有用。我將 cleanmodule 程式碼置於匿名函式中,並呼叫匿名函式。我也在私有模組表格中納入 _G。此程式碼可以放在任何模組檔案的開頭,而且完全不會取代任何函式。在取代值時,它和使用範例 2 有相同的問題,但解決方法相同。
(function (modname) -- Helper function added to modules defined by cleanmodule -- to support importing module symbols into client namespace. -- Usage: -- local mm = require "mymodule" -- only local exported -- require "mymodule" () -- export module table to environment -- require "mymodule" ":all" -- export also all functions -- to environment. -- require "mymodule" (target,":all") -- export instead to given table local function import(public, ...) -- Extract arguments. local target, options = ... if type(target) ~= "table" then target, options = nil, target end target = target or getfenv(2) -- Export symbols. if options == ":all" then for k,v in pairs(public) do target[k] = v end end -- Build public module tables in caller. 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} -- public namespace for module local priv = {_PUBLIC = pub, _PRIVATE = priv, _NAME = modname, _G = _G } -- private namespace for module 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)(...)
使用範例 4 - 這是修改版的範例 2。這在現有 module
函式架構 (不取代 module,也不取代 require) 中獲得公開/私有名稱空間。然而,它未解決 module
函式寫入 _G 而不是寫入客戶端的私有環境的問題 (這可以視為一個平行問題,可以藉由重新定義 module
來解決)。
-- package/clean.lua -- -- To be used as an option to function module to expose global -- variables to the private implementation (like package.seeall) -- but not expose them through the public interface. -- -- Changes the environment to a private environment that proxies _G. -- Writes to the private environment are trapped to write to both -- the private environment and module (the module's public API). -- -- Example: -- -- -- baz.lua -- module(..., package.clean) -- function foo() print 'test' end -- function bar() foo() end -- -- Now, a client using this module -- -- require "baz" -- assert(not baz.print) -- globals not exposed (unlike package.seeall) -- baz.bar() -- ok -- -- Careful: Redefinitions will not propogate to module. Allowing that -- would require making the private environment an empty proxy table. -- -- Note: this addresses only one aspect of the problems with the module -- function. It does not addess the global namespace pollution issues. Doing -- so likely requires redefining the module function to write to the client's -- private environment rather than _G, or avoiding -- the module function entirely using a simple table approach [1]). -- -- [1] https://lua-users.dev.org.tw/wiki/ModuleDefinition -- -- Released under the public domain. David Manura, 2009-09-14. 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
-- package/veryclean.lua -- -- This is similar to package.clean except that the public interface is -- maintained in a separate table M, even in the private implementation. -- -- Example: -- -- -- baz.lua -- module(..., package.veryclean) -- function M.foo() print 'test' end -- function M.bar() M.foo() end -- -- This makes public methods more explicit and also simplifies -- the implementation. -- -- Released under the public domain. David Manura, 2009-09-14. function package.veryclean(module) local privenv = {M=module, _PACKAGE_VERYCLEAN = true} setfenv(3, setmetatable(privenv, {__index=_G})) end return package.veryclean
-- package/strict.lua -- -- Here's an optional replacement for strict.lua compatible with -- package.clean and package.veryclean. Example: -- -- module(..., package.veryclean, package.strict) -- -- Released under the public domain. David Manura, 2009-09-14. 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
使用範例 5 - 改良於取用範例 4 的 package.clean()。使用代理表格解決重新宣告問題。繼承 CLEAN_ENV 而不是 _G,因此避免看到受污染的全球環境,進而解決模組 () 導入的依賴項隱藏問題。例如,可以在程式開頭,將 _G 的內容複製到 CLEAN_ENV,這樣模組永遠看到的都是乾淨的 Lua 環境,而不含任何外部引進的依賴項。
-- kinda bloated at 4 tables and a closure per module :) 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
使用範例 6 改編自使用範例 1,也就是第一個範例,並參考範例 4 的靈感,將 module 和 seeall 分割成平行函式。這裡,我們使用單一表格作為模組名稱空間,以避免與雙系統的所有同步問題。私有模組環境是一個空的代理表格,其擁有自訂尋找例程 (_M[k] 或 _G[k],就是這樣)。私有尋找中的間接運算假設,對外部來說,模組尋找的速度比對內部來得重要 (您可以在內部使用 local)。
-- clean.lua -- Adaption of "Take #1" of cleanmodule by Ulrik Sverdrup -- My additions are in the public domain -- -- Functions: -- clean.module -- clean.require -- clean.seeall -- Declare module cleanly: -- module is registered in package.loaded, -- but not inserted in the global namespace local function _module(modname, ...) local _M = {} -- namespace for module setfenv(2, _M) -- Define for partial compatibility with module() _M._M = _M _M._NAME = modname -- FIXME: _PACKAGE -- Apply decorators to the module if ... then for _, func in ipairs({...}) do func(_M) end end package.loaded[modname] = _M end -- Called as clean.module(..., clean.seeall) -- Use a private proxy environment for the module, -- so that the module can access global variables. -- + Global assignments inside module get placed in the module -- + Lookups in the private module environment query first the module, -- then the global namespace. local function _seeall(_M) local priv = {} -- private environment for module local privmt = {} privmt.__index = function(priv, k) return _M[k] or _G[k] end privmt.__newindex = _M setmetatable(priv, privmt) setfenv(3, priv) end -- NOTE: Here I recommend a rawset version of -- https://lua-users.dev.org.tw/wiki/SetVariablesAndTablesWithFunction -- But it is left out here for brevity. -- Require module, but store module only in -- private namespace of caller (not public namespace). local g_require = require local function _require(name) local result = g_require(name) rawset(getfenv(2), name, result) return result end -- Ironically, this module is not itself clean, so that it -- can be used with 'require' module(...) module = _module seeall = _seeall require = _require
-- Ulrik,2010apr
使用範例 7 Lua 5.2 的可能模組宣告
-- init.lua 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 -- init-2.lua 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 -- module1.lua return module("my mega module",safeenv{trace=print},function(_ENV) -- safe module. there are no load require ... even no print a=20 -- public var local b=30 -- private var function dump(x) for k,v in pairs(x) do trace(k,v) end end local function do_something() a=a+1 end -- private function end) -- module2.lua return module("some description",_G,function(_ENV) -- see all module public_var=12345 local private_var=54321 public_fn=print local private_fn=print end) -- test1.lua local m1=require "module1" m1.dump(m1)