最近 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 には影響はない