Luaでusingディレクティブを実現する2

id:Akiva:20110120#1295539117[Luaでusingディレクティブを実現する1]の続きです。


昨日のエントリーで大体前置きは書いたので、コードを貼っておきます。
これもLevライブラリに取り込む予定なので本来MITライセンスですが、
掲載分のコードはパブリックドメイン扱いで結構なので適当にコピーして使って下さい。


最新版は、
http://levana.svn.sourceforge.net/viewvc/levana/trunk/lev/util.lua
からダウンロード可能です。(こっちは一応MITライセンス)
今回はリビジョン25に日本語でコメント入れておきます。

-----------------------------------------------------------------------------
-- Name:        util.lua
-- Purpose:     便利関数の定義
-----------------------------------------------------------------------------

-- 使用方法:
--   * using() : ルックアップ設定のクリア
--   * using(...) : ルックアップ設定の追加、但し ... はテーブルのリストとする

-- 依存関係
local _G = _G

-- モジュール登録
module 'util'


-- テーブル t から value を検索し、見つけたら削除
local find_and_remove = function(t, value)
  for i,j in _G.ipairs(t) do
    if (j == value) then
      _G.table.remove(t, i)
      return
    end
  end
end


-- テーブル(配列) t を逆順にしたものを返す
local reverse = function(t, ...)
  local r = {}
  for i,val in _G.ipairs(t) do
    _G.table.insert(r, 1, val)
  end
  return r
end


-- 変数名 varname で変数値のルックアップ
local lookup = function(env, varname)
  meta = _G.getmetatable(env)
  -- 最初はグローバル変数から探す
  if meta.__owner[varname] ~= nil then
    return meta.__owner[varname]
  end
  -- 無ければリストに登録されたテーブルから順番に探す
  for i,t in _G.ipairs(meta.__lookup) do
    if t[varname] ~= nil then
      return t[varname]
    end
  end
  -- それでも無ければ一段外のスコープの環境から探す
  if (meta.__parent == meta.__owner) then
    -- 一段外のスコープとグローバル環境が同じなら探す意味が無い
    -- non sense to look up twice for the same table
    return nil
  end
  return meta.__parent[varname]
end


-- 変数名 key で 値 value をグローバル変数として代入
local substitute = function(env, key, value)
  meta = _G.getmetatable(env)
  meta.__owner[key] = value
end


-- 代替usingディレクティブ
function using(...)
  -- 最初に呼び出し元の環境と、環境のメタテーブルを取得
  local env  = _G.getfenv(2)
  local meta = _G.getmetatable(env) or {}
  -- 呼び出し元(関数)のアドレスも取得しておく
  local f = _G.setfenv(2, env)
  if (meta.__caller == f) then
    -- 既に環境のセットアップ済みなので、ルックアップ設定のみ変更する
    if #{...} == 0 then
      -- もしルックアップ指定が無い(usingの空呼び出し)なら、設定を削除する
      meta.__lookup = {}
      return env
    end
    for i,val in _G.ipairs({...}) do
      find_and_remove(meta.__lookup, val)
      _G.table.insert(meta.__lookup, 1, val)
    end
    return env
  end

  -- 環境未設定なので新しい環境テーブルとメタテーブルを用意する
  local newenv  = {}
  local newmeta = {}
  -- メタテーブルに必要な項目を設定する
  newmeta.__caller = f
  newmeta.__index = lookup
  newmeta.__lookup = reverse({...})
  newmeta.__newindex = substitute
  newmeta.__owner = meta.__owner or env
  newmeta.__parent = env
  -- 新環境とメタテーブルを登録
  _G.setmetatable(newenv, newmeta)
  _G.setfenv(2, newenv)
  -- 新環境を返す
  return newenv
end


-- usingは最初からプレフィックス無しで呼べた方が嬉しい
_G.using = using


使い方はこんな感じで、

require 'util'

t1 = {x = 'x1', y = 'y1'}
t2 = {y = 'Y2', z = 'Z2'}

function test()
  using() -- 安全のためルックアップ設定のクリア
  -- 初期化しておく
  x, y, z = nil
  print(x, y, z) -- nil nil nil

  using(t1)
  print(x, y, z) -- x1 y1 nil

  using(t2) -- using(t1, t2)としても同じ
  print(x, y, z) -- x1 Y2 Z2

  using(t1) -- using(t2, t1)としても同じ
  print(x, y, z) -- x1 y1 Z2

  x = 'XXX'
  print(x, y, z) -- XXX y1 Z2
end

test()
test()
print(x, y, z) -- XXX nil nil


実行結果から分かるのは、

  • グローバル変数が存在する場合は、グローバル変数が優先される
  • グローバル変数が存在しない場合、usingされたテーブルから変数値が引用される
  • (グローバル変数が存在しない場合で)usingされたテーブルの変数名が衝突する場合は、後にusingされた方が優先される
  • スコープ外(関数外)ではスコープ内(関数内)のusingは影響しない


ただし、環境の設定が適用される「スコープ」というのが他の言語とちょっと違うので語弊があるのですが、do 〜 end ブロック内でusingした場合は、(恐らく予想に反して)ブロック外からでもアクセスできちゃいます。ただしファイル単位ではちゃんとスコープとして環境設定できるので、外部コードをrequireとかdofileした時のusingはちゃんと内部で完結されます。


また、上記の例では関数の最初にusing()という空呼び出しをしていますが、
これをしない場合は関数の再呼び出し時にもルックアップの設定が引き継がれます。
多くの場合は必要無いのですが、例えば

function f()
  newt = {x = 'hogehoge', y = 'fugafuga', z = 'piyopiyo'}
  using(newt)
  〜 -- newtへアクセスするコード
end


とした場合、一見普通に動いてようで、関数呼び出しの度に新しいテーブルを作ってルックアップ対象に登録するので、
メモリ使用量が一方的に増加してGCがうまく機能できなくなります。
なので、

  • ルックアップ設定が関数終了後も残るのでは困る場合
  • 新しく生成されたテーブルをルックアップ設定したい場合

は、どこか(普通は冒頭)でusing()すべきで、そうでない場合もやはりusing()した方が安全です。
基本的に何度もusingされる事を前提にコーディングしているので、呼び出しコストはそんなに大きくないと思います。
解説がくどすぎる感じですが、だいたいそんな感じです。