Lua で using ディレクティブみたいなもの

最近 Lua でいろいろ書いてるのですが、 Lua の不満な点として、
標準ライブラリが全体的に最低限の機能しか無い点が挙げられます。


いや、 Lua という言語のコンセプトから考えると、これば別に不満点ではないですが、
それでも、ちょっと便利に使おうと思うと、大量の補助関数が必要になってしまいます。


ただ、幸いにも Lua はモジュールの仕組みがしっかりしているので、
自分で便利関数をまとめたモジュールを用意してやればよく、
例えば、僕は Oven とかの Range ライブラリが好きなので、
誰でもすぐ書けるようなちょっとしたアダプタを作って、

function is_odd( n ) return n % 2 == 1 end

-- filtered とか
for i in ranges.filtered( is_odd, ranges.irange(10) ) do
  -- 1 から 10 までの奇数に対して処理
end

-- head みたいなのとか
for line in ranges.take( 10, io.lines("file") ) do
  -- file の先頭10行に対して処理
end

-- 無限リストとか
for i in ranges.take( 10, ranges.filtered( is_odd, ranges.irange() ) ) do
  -- 全ての奇数の中から 10 個取ってきて処理
end

こんな感じで書いてるわけですが、
正直毎回 ranges. とか書くの面倒ですよね。


勿論、 Lua では関数はファーストクラスですから、適当に代入してやればいいのですが、
インタプリタとかで気軽ーに使いた場合は、かなり面倒だったりします。


また適当に変数に束縛した場合、
モジュールをリロードして関数が新しくなった場合であっても、
依然として古い関数を使い続けることになったりして、テストには不向きです。


これを解決するためには、あるモジュールの中身を、修飾なしで lookup する仕組みが欲しい。
幸いにも Lua には __index メタメソッドという仕組みがあるので、
何とかして出来ないものでしょうか。


というわけで、作ってみました。

-- modules.lua

-- 依存関係
local require, package = require, package
local getfenv = getfenv
local getmetatable, setmetatable = getmetatable, setmetatable
local ipairs = ipairs
local type = type
local assert, error = assert, error

module( ... )

-- モジュールの再ロード
-- 今回の本質じゃないけど便利なので。
function reload( module )
  package.loaded[module] = nil
  return require(module)
end

-- 本体
do
  local get_mixed_modules, mixin_impl_;
  -- モジュールを「混ぜる」
  function mixin( to, module, ... )
    assert( to ~= nil and module ~= nil, "at least 2 arguments required." )
    -- to のメタテーブルから、混ぜられたモジュールのリストを取ってくる
    local modules = get_mixed_modules(to)
    
    -- 登録
    return mixin_impl_( modules, module, ... )
  end
  
  -- module を登録する
  function mixin_impl_( modules, module, ... )
    if module == nil then return end
    if type(module) == "string" then
      module = require(module)
    end
    
    -- 自己組織化検索みたいな。
    -- module を先頭に持ってくる
    local i = 1
    local temp = module
    
    while true do
      local m = modules[i]
      if m == nil or m == module then break end
      
      modules[i], temp = temp, m
      i = i + 1
    end
    
    modules[i] = temp
    
    -- 再帰
    return mixin_impl_( modules, ... )
  end
    
  local find_from_modules
  -- メタテーブルを検索してモジュールリストを得る
  -- ないなら作る
  function get_mixed_modules( module )
    local mt = getmetatable(module)
    if mt == nil then
      mt = {}
      setmetatable( module, mt )
    end
    
    -- メタテーブルの __mixed_modules が mixin されたモジュール
    local mixed_modules = mt.__mixed_modules
    if mixed_modules == nil then
      -- __mixed_modules がなければ作る
      mixed_modules = {}
      mt.__mixed_modules = mixed_modules
      
      -- __index メタメソッドを定義
      local __index_old = mt.__index
      local __index_new
      
      -- 昔の __index が何かで処理を切り分ける
      if __index_old == nil then
        -- nil なら、そのまま検索
        __index_new = function( t, k )
          return find_from_modules( mixed_modules, k )
        end
      elseif type(__index_old) == 'function' then
        -- 関数なら
        __index_new = function( t, k )
          -- まずモジュールから検索して
          local found = find_from_modules( mixed_modules, k )
          if found ~= nil then return found end
          -- 見つからなかったら昔の奴を使う( strict.lua 対策)
          return __index_old( t, k )
        end
      else
        -- それ以外なら
        __index_new = function( t, k )
          -- まずモジュールから検索して
          local found = find_from_modules( mixed_modules, k )
          if found ~= nil then return found end
          -- 見つからなかったら昔の奴を使う
          return __index_old[ k ]
        end
      end
      -- 設定
      mt.__index = __index_new
    elseif type(mixed_modules) ~= 'table' then
      -- テーブルでないとおかしい。
      error( "collision found in metatable '__mixed_modules'." )
    end
    
    return mixed_modules
  end
  
  -- モジュールリストから対象の名前を捜す
  -- 衝突は検出しない。最も最近に mixin されたモジュールが優先される
  function find_from_modules( modules, key )
    for _, module in ipairs(modules) do
      local found = module[key]
      if found ~= nil then return found end
    end
    
    return nil
  end
  
  
  -- 補助関数
  -- mixin によって「混ぜられた」モジュールのリストを得る
  -- このリストをいじることで検索結果を変えられる
  function mixed_modules( module )
    local mt = getmetatable( module )
    return mt and mt.__mixed_modules
  end
end


-- 気軽に使えるようヘルパ関数を用意する
-- 現在のモジュールに対象モジュールを「混ぜる」
do
  local mixin = mixin
  
  function using( module, ... )
    assert( module ~= nil, "module expected." )
    return mixin( getfenv(2), module, ... )
  end
end

使い方はこんな感じです。

require "modules"  -- まず require
modules.using( modules ) -- とりあえず modules の全要素を可視化
using( io, string, table, math ) -- やたらめったら取り込む
using "ranges" -- 読み込まれてないモジュールならば読み込んでくれる

-- 俺ライブラリ ranges.iragne とかを使う。
for i in irange(10) do ...


細かい動作とか:

t1 = { x = 1, y = 2 }
t2 = { x = "hoge", z = "fuga" }
x, y, z = nil  -- 念のため消しておく

using(t1)
print( x )  -- 1
print( y )  -- 2

using(t2)
print( x )  -- hoge. あとに mixin した方が優先される
print( y )  -- 2. 勿論隠されない奴はそのまま使える

x = 0       -- 代入すると
print( x )  -- x は 0 になったが
print( t1.x, t2.x ) -- 1 hoge. t1 や t2 には影響はない