Lua

Lua で複数のメンバ変数を配列のように扱う方法(例:v[1] では v.X を、v[2] では v.Y を表すなど)

More than 1 year has passed since last update.

この文書の目的

例えば、3次元ベクトルオブジェクト v を使用するとき、X,Y,Z など座標軸を意識して書く場合は v.X などと書くのが自然だと思います。しかし、その一方で、行列との掛け算を行う場合は、メンバ変数 X,Y,Z をひとつの配列のように取り扱うことができると便利です。

本文章では、ベクトル型の変数 v に対し、メンバ変数 X を v[1] としても取り扱う方法について説明します(…とはいえ、実際には、ベクトル型以外に使用される場面は少ないかもしれませんが、Lua でも実現できる、ということで記しておきます)。

動作確認した Lua のバージョン

Lua-5.3.4。メタテーブルが使える Lua のバージョンであれば、多分使えると思います。

しくみの概略

Lua におけるオブジェクトのインスタンス(といっても単なるテーブルですが)に対し、添字のオペレータを適用する場合、そのテーブルのメタテーブルに設定された関数で添字の情報を取得することができます。

本文書では、その仕組みを活用して、v[2] と書けば、v.Y を表すような仕組みを実現すします(ちなみに v.X は v[1] を、v.Z は v[3]と表すこととします)。

メタテーブル __index と __newindex の仕様

テーブルに対し、指定されたメンバを持っていない場合、__index や __newindex が参照されます。この仕組を活用し、型定義のためのテーブルをこれらのフィールドに指定することにより Lua のオブジェクト指向が実現されるのは周知の通りです。

__index や __newindex にはテーブルしか設定できないのか?といえば、そうではありません。関数を設定することも可能です。その場合、テーブルに存在しないメンバにアクセスするとき、設定した関数が呼び出されます。

これらの関数に渡される引数ですが、__index では第 1 引数に対象となっているテーブルが、第 2 引数には見つからなかったメンバ名がそれぞれ入っています。

getterFunction
t={}
setmetatable(t,{__index=function(inSelf,inIndex)
                            print("index="..inIndex)
                        end})
t.A -- "index=A" と表示される
t[3] -- "index=3" と表示される

__newindex では、上記の引数に加え、第 3 引数として設定しようとしている値が渡されます。

setterFunction
t={}
setmetatable(t,{__newindex=function(inSelf,inIndex,inValue)
                              print("index="..inIndex)
                              print("value="..inValue)
                           end})
t.A=1 -- "index=A" および "value=1" と表示される
t[3]=10 -- "index=3" および "value=10" と表示される

複数のメンバ変数を配列のように扱う方法

サンプルで使用するオブジェクトの仕様

本文書では、X,Y,Z という 3 つのメンバ変数を持つ Vector3 というオブジェクトを用いて説明を行います。それぞれのメンバ変数には、v[1]〜v[3] としてもアクセスできることとします。もちろん、メンバ変数を直接指定する場合でも、配列として扱う場合でも、どちらでも読み書きできるものとします。

vector_example.lua
-- Vector3 の使用例
v=Vector3.new(1,2,3) -- メンバ変数 X,Y,Z にそれぞれ 1,2,3 を設定
print(v.X) -- 1 と表示される
v.X=10
v[2]=20
for i=1,3 do print(v[i]) end --- 10 20 3 とそれぞれ改行して出力される

Vector3 型の定義

例えば、以下のようなコードを書くことにより、上で定義した振る舞いをする Vector3 型が実現できます。

Vector3
Vector3={}
Vector3.new=function(inX,inY,inZ)
    if inX==nil or inY==nil or inZ==nil then
        error("invalid argument.\n"
            .."Vector3.new needs 3 arguments. ex: v=Vector3.new(1,2,3)")
    end

    local ret={}
    ret.X=inX
    ret.Y=inY
    ret.Z=inZ

    ret.Dump=function(inSelf)
        print("{X="..inSelf.X..", Y="..inSelf.Y..", Z="..inSelf.Z.."}")
    end       

    setmetatable(ret,{
        __index=function(inSelf,inIndex)
                    if inIndex==1 then
                        return inSelf.X
                    elseif inIndex==2 then
                        return inSelf.Y
                    elseif inIndex==3 then
                        return inSelf.Z
                    else      
                        error("getter: invalid index="..inIndex)
                    end                       
                end                           
        ,
        __newindex=function(inSelf,inIndex,inValue)
                    if inIndex==1 then        
                        inSelf.X=inValue      
                    elseif inIndex==2 then    
                        inSelf.Y=inValue      
                    elseif inIndex==3 then    
                        inSelf.Z=inValue      
                    else                      
                        error("setter: invalid index="..inIndex)
                    end
                end
        })

    return ret
end

応用例

Vector3 型のメンバに配列として読み書きできるようになっているので、行列との掛け算などもループを使って書くことができます。

Matrix3x3.lua
Matrix3x3={}
Matrix3x3.new=function(inA11,inA12,inA13,
                       inA21,inA22,inA23,
                       inA31,inA32,inA33)
    local ret={{inA11,inA12,inA13},
               {inA21,inA22,inA23},
               {inA31,inA32,inA33}}

    ret.Apply=function(inSelf,inVec)
        local v=Vector3.new(0,0,0)
        for i=1,3 do
            local t=0
            for j=1,3 do
                t=t+inSelf[i][j]*inVec[j] -- 配列として読みとり
            end
            v[i]=t -- 配列として代入(書きこみ)
        end
        return v
    end

    ret.Dump=function(inSelf)
        for i=1,3 do
            print(inSelf[i][1].." "..inSelf[i][2].." "..inSelf[i][3])
        end
    end

    return ret
end

上記で定義した Matrix3x3 と Vector3 は、以下のように使用することができます。

MatVec.lua
v=Vector3.new(1,2,3)
v:Dump() -- {X=1, Y=2, Z=3} と表示される

-- m に X 要素と Z 要素を入れ替える行列を設定する
m=Matrix3x3.new(0,0,1,
                0,1,0,
                1,0,0)

t=m:Apply(v)
t.Dump() -- {X=3, Y=2, Z=1} と表示され、正しく計算できていることが確認できる

まとめ

本文書では、メタテーブルの __index および __newindex に関数が指定できることを紹介し、その応用として複数のメンバ変数を配列のように扱う方法を説明しました。

同様の仕組みで、Lua でも C# のプロパティのようなものを定義することができると思います。こちらについても、機会があれば別途文章を書いてみたく思います。

もし、この記事が役に立ったとか、面白いと思われましたら、「いいね」やストック、フォローしていただければ嬉しいです。Qiita で文章を公開する励みになりますので、よろしくお願いします。