Luaは殆どのオブジェクトが自由に上書き可能なテーブルであるためかなり柔軟なメタプログラミングが可能な言語であるが、言語のセマンティクスの内側からはローカル変数自体を触ることができない。しかし、例えばRubyの文字列展開のようなものをプリプロセッサを使わずに実装しようとすると、どうしてもスタックフレーム中にあるローカル変数にアクセスしたくなる。printf使えば済むとか、LISPのマクロやTemplate Haskellなどによる実装の方が好ましいとか、思わなくもないが、まあとにかくLuaで何とかしたいこともある。
debug.hogeでアクセス可能なデバッグライブラリの機能を使うと、何とかすることが可能であった。割と整理された関数が提供されているものの、デバッグ向けのライブラリであるためにインタプリタの実装に近い部分のモデルに従った機能が提供されており、何をするものか少し想像が付きづらかった。公式のドキュメントでは、一度分かってしまうまでは分からない類の解説がなされており、わたしは始め読んでいて何を言っているのか分からなかった。
ここでは、Lua 5.2のデバッグライブラリを用いた変数周りのリフレクションについて、背景に有りそうだと感じられる概念を含めて書いておく。Lua 5.1とは多分微妙に違う。なおスレッドやコルーチン周りについては(わたしが知らないので)完全にスルーする。
まず、Luaのドキュメントを読んでいてenvironmentという言葉が出てくるが、これはSchemeインタプリタにおけるenvironmentとは全く違うので注意である。分かってしまえば大したことはないが、わたしはここで激しくつまづいた。Luaのローカル変数は、Schemeと同じくレキシカルスコープに従っている。Schemeでは、レキシカルスコープに従った変数名と変数の対応付け、つまりLuaで言うところのローカル変数のマッピングをenvironmentと呼ぶが、Luaにおいては_ENVという名前のテーブルをenvironmentと呼び、これはむしろグローバル変数のマッピングとしての役割を果たす。
Luaにはローカル変数とグローバル変数があることを思い出そう。ローカル変数のスコープは単なるレキシカルスコープである。一方、グローバル変数hogeへのアクセスは_ENV.hogeへのアクセスとして扱われる。_ENVは、単なるローカル変数としてアクセス可能なテーブルであり、トップレベルで暗黙に定義されているとみなして良い。よって、グローバル変数は、同じローカル変数_ENVが見える範囲内をスコープとすることになる。
以下の例がこの挙動を示している:
local myprint = print
a = "a"
b = "b"
myprint(a) -- prints "a"
myprint(b) -- prints "b"
do
local _ENV = {
a = "aaa"
}
myprint(a) -- prints "aaa"
myprint(b) -- prints "nil"
end
myprint(a) -- prints "a"
myprint(b) -- prints "b"
単一のソースコードで普通にプログラムを書く際には、ローカル変数だけで大体片がつくのでグローバル変数の概念はさして有用でもない。ただ、他のコードをrequireしたりevalしたりする際に大域的に見える関数や変数が定義される場所としてenvironemntは重要であり、また実際には単一のスクリプトを実行する際にもprintのようなグローバル関数がenvironmentの上に定義されているということになっている。
Schemeでは、この際に「Schemeにおける」environmentを受け渡す。Schemeにおけるenvironmentの普通のS式での表現は自明では無く、environmentは組み込み関数によって取得可能であるが特殊なプリミティブとして扱われる(R5RSではこんな感じだった気がするがちょっと自信無い。またR6RSやR7RSでは違うのかもしれない)。一方、Luaにおいてはテーブルがプリミティブとして存在するのだから、フラットなテーブルを受け渡すと都合が良い。そしてこのテーブルがenvironmentと呼ばれると考えられる。
単なる再帰によるインタプリタなり、VMなり、コンパイルされた機械語の実行なり、どのような実行形式においても変数を保持しておくためのデータ構造は必要である。概念的なレベルで良いのでこのデータ構造を考えると、Luaのデバッグライブラリの機能はすんなりと腑に落ちる。
関数が定義するローカル変数は、各変数ごとにスタックフレーム中でのオフセットを表す数値が割り当てられ、関数が呼び出された際のスタックフレームに保持される。また、関数を実行する際に外側のブロックで定義されているローカル変数にアクセスすることを考えると、現在実行している関数が外側のブロックから抜け出していないなら、スタックを字句的なネストから決まる数だけ上に辿ってアクセスすれば済む。一方、局所関数の定義がブロックの外側に抜け出すならば、変数が保持されていたスタックフレームは存在しないので、クロージャ変換を行い自由変数を捕獲しておくことになる。
デバッグライブラリは、まさにこのスタックフレームやクロージャに対して、何らかの操作を行う関数群を定義している。
スタックをlevelだけ上に辿った場所にある、local番目のローカル変数を操作する。なおgetlocalは変数の名前と値を組にして返す(これを使って、特定の名前を持つローカル変数を探すことができる)。ここで、末尾呼び出し最適化が為されるとスタックフレームが消えるので場合によっては注意が必要。カッコの有無によって末尾呼び出し最適化が変化するということも起こる。
local function f(x)
print(x)
return x + 42
end
function g(y)
return f(y)
end
function h(y)
(return f(y))
end
としてf,g,hを定義すると、g内でのfの呼び出しは末尾呼び出しの最適化の対象となり、h内でのfの呼び出しは末尾呼び出しの最適化にならない(C LuaでもLuaJでも同様の挙動だったが、これがバージョンごとに変化することの無い挙動なのかは不明)。
クロージャfに捕獲されたn番目の自由変数を操作する。普通の変数代入と同じように動作する。
Luaでは変数に再代入することが可能であり、またクロージャに捕獲された自由変数への再代入は、同じ変数を捕獲している別のクロージャにも影響する。よって、一般的に参照を介して自由変数を保持していると言える。
upvalueidは、クロージャfに捕獲されたn番目の自由変数を保持する参照を一意に区別する識別子を返す。upvaluejoinは、f1内のn1番目の自由変数を保持する参照を、f2内のn2番目の自由変数を保持する参照と同じものに置き換える。ここで行うのは単なる変数への代入ではなく、参照自体の置き換えである。