この文書の目的
Lua には定数が存在しません。もちろん、開発の現場における種々のローカルルール - 例えば、「全部大文字の変数は定数として使用するから変更してはダメ」とか(Apple 風に)「kで始まる変数名は定数だから云々」など、プログラマが注意を払うことにより、結果として読取り専用変数を実現することは可能です。
しかし、これらの対処方法では「どうも不安だ」、「なんとなく気持ちが悪い」、等々…という方もいることでしょう。この文書では、メタテーブルを活用した読取専用変数を実現する方法を解説します。
動作確認した Lua のバージョン
Lua-5.3.4。メタテーブルの機能を有する最近の Lua であれば動作すると思います。
しくみ
以前公開した記事「Lua で、プログラムから変数を作る方法」でも説明していますが、グローバル変数の実態は _G に格納されているフィールドそのものです。そして、グローバル変数として _G に格納されていない変数にアクセスする場合、メタテーブルの __index または __newindex が参照されます。
このメカニズムを利用して、まず、定数と同名の _G のフィールドを消去(= nil を代入)しておきます。次に、定数の値そのものは、どこか別のテーブルに格納します。このテーブルを定数テーブルと呼ぶこととします。
読取り時には _G の __index を活用し、定数テーブルから値を返すようにします。書き込み時には __newindex を利用して、定数テーブルに同名の定数が定義されている場合ははエラーを表示するようにします。そうでない場合は、定数テーブルにその値を書き込むようにします。
定数テーブルについてですが、できるだけグローバルな環境を汚したくありません。そのため、初期化時にローカルなテーブルを用意し、それを使用します。
const パッケージ
仕様
実装例として、const パッケージを作ってみようと思います。const パッケージは、const=require("const") として、以下の機能を有するものとします。
const=require("const")
const.define -- 定数の定義(再定義の場合はエラー発生とする)
const.overwrite -- 定数の再定義
const.undef -- 定数の削除
実際には、こんな感じで使用できるようなパッケージを目指します。
> const=require("const")
> const.define("PI",3.14) -- PI=3.14 として定義
> PI
3.14
> PI=3 -- 上書きしようとすると、エラー発生
./const.lua:36: 'PI' is a const.
stack traceback:
[C]: in function 'error'
./const.lua:36: in metamethod '__newindex'
stdin:1: in main chunk
[C]: in ?
> -- define で再定義しようとしても、
> -- 既に定義されているのでエラーとなる
> const.define("PI",3) ./const.lua:36: 'PI' is a const.
stack traceback:
[C]: in function 'error'
./const.lua:36: in metamethod '__newindex'
stdin:1: in main chunk
[C]: in ?
> const.overwrite("PI",4*math.atan(1)) -- PI を再定義
> PI
3.1415926535898
> const.undef("PI") -- 定数から削除
> PI=4 -- もはや定数でなくなったので、普通に代入できる
> PI
4
define の実装
定数の定義や読出しは、メタテーブルに格納した定数テーブルに対する書込みと読出しに対応します。そのため、const.define("PI",3.14) は以下のように処理されます。
-- step 1.
-- 既に定義されている変数に対しては __index や __newindex が適用されないので、
-- グローバル変数としての PI を消去します。
_G["PI"]=nil
-- step 2.
-- 定数テーブルにて PI=3.14 を定義します。
-- 実際には、local constTable={} として定義されている constTable に対し、
-- 以下の文を実行します。
constTable["PI"]=3.14
constTable に格納されている変数は、グローバルな変数としてアクセスされなければなりません。まず、値の読出しに関しては、_G の __index に constTable を設定すれば十分です(step 1 により、定数と同名の変数は定義されていないことが保証されているので)。例えば、上に記した PI の場合ですが、以下のような感じで処理が進められます。
- 変数 PI を参照
- _G に PI というフィールドはあるか? → ない
- _G の __index を参照 → __index はテーブル型
- __index の PI というフィールドを参照 → フィールドは存在し、その値は 3.14
- 3.14 をグローバルな変数 PI の値とする
書込みに関しては、もう少し複雑な処理となります。「Lua で複数のメンバ変数を配列のように扱う方法」で説明したように、__index や __newindex には関数を指定することもできます。
今回も、__newindex に関数を指定し、定数の上書きチェックを行うこととします。この関数で行うべきことは、値を代入しようとしている変数名が constTable のフィールドとして存在しているかどうかを調べ、存在している場合は、「定数だから書き換えできない」などと行ったメッセージとともに error 関数を実行します。constTable のフィールドには存在しない場合=定数でない場合は、その変数に値を代入しないといけないので、rawset 関数を使い、値を代入します。
rawset 関数は、__newindex を起動せずに、直接、変数に値を代入する関数です。詳しくはリファレンスの rawset の説明 を参照して下さい。
overwrite, undef の実装
const パッケージの主なメカニズムは define の実装について述べたとおりです。overwrite と undef については、constTable に対して、値を上書きすることと、該当のフィールドを消去することで実現できます。
実装例
具体的な const パッケージの例を以下に示しておきます。require("const") で使用できるように、ファイル名は const.lua としておきます。
-- const.lua
--[[
example:
const=require("const")
const.define("PI",3.14)
const.overwrite("PI",math.atan(1)*4)
const.undef("PI")
]]
do
local constTable={}
local constPackage={}
constPackage.define=function(inName,inValue)
if constTable[inName]~=nil then
error("const.define: '"..inName.."' is already defined.")
end
_G[inName]=nil
constTable[inName]=inValue
end
constPackage.overwrite=function(inName,inValue)
constTable[inName]=inValue
end
constPackage.undef=function(inName)
if constTable[inName]~=nil then
constTable[inName]=nil
end
end
local setter=function(inTable,inName,inValue)
if inTable==_G and constTable[inName]~=nil then
error("'"..inName.."' is a const.")
end
rawset(inTable,inName,inValue)
end
setmetatable(_G,{
__index=constTable,__newindex=setter
})
return constPackage
end
まとめ
この文書では、定義されていないグローバル変数について、_G の __index と __newindex が getter および setter となることを利用し、読取専用変数(定数シンボル)を実現しました。__newindex で起動される関数内では、再帰的に setter が起動することを防ぐために rawset 関数を活用しました。
実装例のコードでは、定数シンボルもしくは読取専用変数を実現できました。Lua で定数を実現したいと思っている方はぜひとも活用していただければ、幸いです。
参考文献
"Re: Howto set global variable as ReadOnly", Peter Shook, lua-l archive, 2003.