替代模組定義

lua-users home
wiki

在 Lua 中定義「模組」[1]有很多方法。

使用 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 中額外部分區域變數的原因。

localmodule

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 DesignPatterns.)

--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)(...)
--PeterSchwier?,2009Feb04

使用範例 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)

另請參閱


RecentChanges · 偏好設定
編輯 · 歷史
最後編輯時間為 2015 年 2 月 19 日下午 6:01 GMT (diff)