Lua 5.1 のモジュール機能

Lua のモジュール管理機能は 5.1 版になって追加(拡張) されたものであるので、 オンラインで読める初版の 「Programming in Lua」 (Lua 5.0 版を対象とする) では触れられていない。

※ と思ったら、第 2 版でモジュールについて触れられている 15 章だけ Ierusalimschy さんのページ でオンライン公開されていた……。

ここでは、Lua のモジュール機能の概略と、 LuaTeX の luatexbase-modutil パッケージの機能との関係について解説する。

Lua のモジュール

本節では、LuaTeX に依存しない Lua のモジュール機能について述べる。

要点

  • require("foo") で foo モジュールを読み込める。 逆に言うと、モジュールとは require() 関数で読み込めるようなテーブルのことである。 (なお、Lua の文法では、require("foo")require "foo" とも書ける。)
  • モジュール foo が読み込み済であるかは、package.loaded["foo"] が定義されているかで判断さえる。 普通は、この値がモジュール自体(テーブル)である。
  • 慣習的に、モジュール foo は foo という(標準環境の)グローバル変数に格納される。 例えば、LuaTeX では標準で lpeg モジュールが読み込まれているので、 lpeg というグローバル変数はこのモジュール(テーブル)を指している。 (また package.loaded["lpeg"] も同じテーブルを指している。)

(※ なお、luatexbase.require_module() を用いる場合は、 必ずグローバル変数への格納を行う必要がある。)

モジュール名は foo.bar のような複合名の場合もある。 この場合、対応する「グローバル変数」 foo.bar というのは、 グローバル変数の foo にあるテーブルの中のキー bar ということになる。

モジュールの読込

require("foo") は以下の手順を実行する。

  1. 既に foo モジュールが読み込み済の場合は何もせず終了: つまり package.loaded["foo"] が存在する場合はその値を返して終わり。
  2. foo モジュールを読み込む:以下の手順からなる。
    • foo モジュールローダー(関数)を取得する。仮にこの値を loader とする。
    • loader("foo") を実行する。通常は、以下の何れかが起こる。
      • (i) その関数実行の戻り値が foo モジュール(テーブル)である。
      • (ii) その関数実行中に package.loaded["foo"] に foo モジュールが代入される。
  3. foo モジュールを戻り値として終了: すなわち、モジュールローダーが何かの値を返した場合は、package.loaded["foo"] にそれを代入し、そして(全ての場合に)package.loaded["foo"] の最終的な値を戻り値とする。

3. の動作は、(i) と (ii) の何れの場合にも「foo モジュール」 を戻り値と package.loaded["foo"] の値に設定する。 もし、(i) と (ii) のどちらも行われないという例外的な場合では、 true をこの両者の値として用いる (だから foo モジュールはやはり読み込み済になる)。

require() はグローバル変数 foo については全く何も関知しないことに注意。 モジュールローダーがグローバルの foo にモジュールを格納するかはローダー次第である。 もしグローバルに格納したのであれば、 以下のようにモジュール foo を使用できる:

require "foo"
foo.some_func()

しかし、格納しない(という慣習を用いる) のであれば、呼び出し側が適当な処置を行う必要がある:

foo = require "foo"
foo.some_func()

モジュールは単なるテーブルなので、別の変数に代入する等の処理が自由に行える。

require "foo"
local F = foo
-- あるいは local F = require "foo" でも同じ
F.some_func()

モジュールローダーの取得

モジュールローダーの取得には、モジュールローダー検索関数を用いる。 モジュールローダー検索関数は複数あり、配列 package.loaders に格納されている。 従って、モジュールローダーの取得の手順は以下のようになる。

  • 配列 package.loaders の各々の値(添字順)を(仮に) searcher として以下を行う。
    • searcher("foo") を実行し、何らかの値が返ったならば、 それをモジュールローダーとして終了(反復打ち切り)。
  • どの検索関数もローダーを返さなかったのでエラー終了する。

Lua の既定の検索関数は以下の通り。 括弧内はその関数の実行で参照される変数。

  • package.loaders[1] : プレロードから検索。 (package.preload
  • package.loaders[2] : Lua モジュールファイル(例えば foo.lua)の検索。 (package.path
  • package.loaders[3] : C モジュールファイル(例えば foo.dll)の検索。 (package.cpath
  • package.loaders[4] : C モジュールのオールインワンローダー。 (package.cpath

(※ ただし、LuaTeX では 2 番目以降のものが、 Kpathsea を利用して同等のファイル検索を行うものに置き換えられている。)

package.loaders[2] について補足しておく。 例えば package.path が次のような値だったとする (Windows 上の動作を仮定する)。

C:\lua\?.lua;C:\lua\?\init.lua

この場合、package.loaders[2]("foo") は以下のファイルを探索する。 (最初に見つかったものを用いる。)

  • C:\lua\foo.lua
  • C:\lua\foo\init.lua

複合名である場合、例えば package.loaders[2]("foo.bar") は以下のファイルを探索する。 ドット(.)がパス区切り(Windows なら \) に置き換えられることに注意。

  • C:\lua\foo\bar.lua
  • C:\lua\foo\bar\init.lua

このようにして取得した Lua モジュールファイルの中身は、 「モジュールローダーの関数の本体」である。 package.loaders[2] の戻り値はこの関数をコンパイルした結果の関数オブジェクトである。

先述の require() の解説で解るように、 モジュールローダーにはモジュール名「"foo"」 が引数に渡されているが、これは可変引数「...」 を利用して読み取ることができる。

Lua モジュールファイルの書き方

基本的に以下の手順を踏む。

  • モジュールとなるテーブルを作成する
  • 自分が用いる慣習に従って 「require() を正しく働かせるための処理」を行う。

(i) を採用する場合:

-- モジュールとなるテーブルを構成する
local m = {}
function m.some_func() ...... end
-- それを戻り値とする
return m

(ii) を採用する場合:

-- モジュールとなるテーブルを構成する
local m = {}
function m.some_func() ...... end
-- package.loaded に代入
package.loaded["foo"] = m

グローバル変数 foo にモジュールを格納するという慣習に従う場合は、 foo = m を実行することになる (あるいは最初から foo = {} として m の代わりに foo を用いて後の記述を行ってもよい。)

モジュールのファイル全体がローダー関数の本体で、 それにモジュール名が渡されているという性質を利用すると、 ファイルにモジュール名を記述する必要がなくなる。

local name = ...           -- 第 1 引数がモジュール名である
local m = {}
package.loaded[name] = m   -- (ii) の場合 
_G[name] = m               -- グローバルに格納する場合

m.greet_word = "Hello"
function m.greeting(n)
  return m.greet_word .. ", " .. n .. "!"
end
function m.greet(n)
  print(m.greeting(n))
end

module() 関数

上の方法だと、モジュール内の公開の変数(関数も含む)に (自身の内部で)アクセスする時に、いつでもテーブルを示す変数名を付す (m.greet_word のように)必要が生じる。

この問題については、Lua の「関数の環境の切替」 (setfenv())を用いた解決法がある。 ローダー関数の環境をモジュール自身に切り替えると、 それ以降の「グローバルな定義」をモジュールに対する定義とすることができる。

local name = ...           -- 第 1 引数がモジュール名である
local m = {}
package.loaded[name] = m   -- (ii) の場合 
_G[name] = m               -- グローバルに格納する場合
setfenv(1, m)              -- 「1」は現在実行中の関数を示す
-- これ以降は「グローバル」がモジュール自身となる

greet_word = "Hello"
function greeting(n)
  return greet_word .. ", " .. n .. "!"
end
function greet(n)
  print(greeting(n))     -- ただしこれはダメ!
end

この構成はある程度「標準的」と考えられるので、 Lua では 2~5 行目の操作を標準ライブラリ関数 module(name) として提供している。 従って、最初の 5 行の代わりに次の 1 行で済ませられる。

module(...)

(もちろん、module("foo") のように名前を指定してもよい。)

しかし、実は上のままだと、 greet() (グローバルには foo.greet()) は動作しない。 なぜなら、setfenv() した後では、 標準のグローバル環境が見えなくなっているので、 標準ライブラリの print() にアクセスできないからである。

標準ライブラリへのアクセスを可能にする簡単な方法は、 モジュールのテーブルを標準グローバル環境(_G) の継承オブジェクトとすることである。 つまり、setfenv(1, m) の前に次を実行する。

setmetatable(m, { __index = _G }) -- m を G の継承とする

これで、print() は標準グローバル環境のものにアクセスされ、 なおかつ、 新たに定義されるものは標準グローバル環境ではなくモジュールに格納されるようになる。

そして、module() 関数を利用する場合でも、 この設定を行う手段が用意されている。 module() の第 2 引数に package.seeall を渡せばよい。

module(..., package.seeall)  -- この 1 行だけで全て上手くいく!

greet_word = "Hello"
function greeting(n)
  return greet_word .. ", " .. n .. "!"
end
function greet(n)
  print(greeting(n))
end

LuaTeX で注意すべき点

LuaTeX のモジュールローダー検索関数

package.loaders[2]("foo") は以下の Kpathsea 検索で見つかるファイルを用いる。

kpsewhich --progname=luatex --format=lua foo.lua

複合名の場合はよく解らないが、 動作としては次のようになっているように見える。 例えば package.loaders[2]("foo.bar") は以下の Kpathsea 検索を順に行い最初に見つかるファイルを用いる。

kpsewhich --progname=luatex --format=lua foo/bar.lua
kpsewhich --progname=luatex --format=lua foo.bar.lua

format=lua の場合に用いられる検索パスの Web2C 変数は LUAINPUTS である。

以上で述べたことは C モジュールローダー (package.loaders の 3~4 番目)でも同様だと思われる (確かめていないが)。 C モジュールについて、 kpsewhichformat の値は clua、 検索パスの変数は CLUAINPUTS である。

luatexbase-modutils に適した Lua モジュール

ここでは、luatexbase-modutils パッケージを使う場合の Lua モジュールの記述法について検討する。 なお、「資料-luatexbaseについて」での解説を前提とする。

luatexbase.require_module("foo") (または \RequireLuaModule{foo} ) は中で、require("foo") を呼ぶので、 luatexbase.require_module() によって呼ばれるものは通常の意味での Lua のモジュールでなければならないが、 その他にもいくつかの制約がある。

まず、luatexbase.require_module()require() と異なり戻り値をもたない (TeX の \RequireLuaModule の方に「戻り値」 を持たせる術がないので当然) ので、グローバル変数への代入が必須となる。 従って、呼び出し側は以下のようになる。

(TeX 中で)
\RequireLuaModule{foo.bar}
(Lua 中で)
luatexbase.require_module("foo.bar")
(以降、グローバル変数 foo.bar がモジュールを指す)

先述の LuaTeX のローダー検索関数の動作より、 foo.bar パッケージは $LUAINPUTS 以下の foo/bar.lua または foo.bar.lua に記述することになる。

グローバル代入が必須ということを考えると、 結局 Lua の module() 関数を使うのが最も簡潔でまた妥当なようだ。 あと、luatexbase.require_module() での検査に備えて、luatexbase.provides_module() を実行する必要がある。 この引数での name 指定は、そもそも 「呼び出された名前と一致するかを検査する」 ためのものなので、「...」 から読み出すのではなく直接記述する。 (provides_module() の引数はできるだけ直接的な形で (値を変数で表す等はせず)書くのが望ましいと思う。) 対して、module() の引数は「...」 でもよいのかも知れない。 なお、module() はモジュール名を 「グローバル変数(実際にはモジュール内)」の _NAME に格納するので、 それ以降でモジュール名が必要になる場合は、 「...」ではなく _NAME を使うのがいいかも知れない。

luatexbase.provides_module({
  name = 'foo.bar',
  date = '2011/01/01',
  version = '0.1a',
  description = 'Some foo and bar',
})
module(..., package.seeall)
local err, warn, info, log = luatexbase.errwarinf(_NAME)

余談

ところで、luatexbase パッケージの Lua モジュールは少々奇妙なことをしている。 例えば、luatexbase-modutils をみる。

luatexbase-modutils.sty の中では、

require('luatexbase.modutils')

で Lua モジュールを読み込んでいる (当然ここで require_module() は使えない)。 しかし、Lua モジュールの中では、

module("luatexbase", package.seeall)
-- "luatexbase.modutils" ではない!

となっている。 つまり、標準環境に luatexbase というモジュールが作られ、 後のグローバル定義はそこに格納される (だから luatexbase.require_module() なのであって、 luatexbase.modutils.require_module() にはなっていない)。 同時に、package.loaded["luatexbase"] にも登録される。 しかし、そのコードの中で、「luatexbase.modutils」 というモジュール(テーブル) はいかなる方法であっても作られず、 当然、グローバルの luatexbase.modutilspackage.loaded["luatexbase.modutils"] にも何も行っていない。

つまり、この「モジュール」は、(i) にも (ii) にも従っていない (そもそも指定されたモジュールを作ってさえいない)。 かなり奇妙な事態であるが、しかし不正ではない。 この場合、package.loaded["luatexbase.modutils"] は true が入ることになる。

(ついでに言っておくと、luatexbase パッケージ全体を読み込むと、 module("luatexbase", package.seeall) が何度も実行されることになるが、 これも別に問題にはならない。 2 回目以降の module() では新たにテーブルは作られず、 1 回目に作られたテーブルが再利用され、 従ってそこに新たな定義が加わることになる。