Lua
オブジェクト指向
スクリプト

Lua で、self.name と書かなくても name だけでメンバ変数やメソッドにアクセスできるようにする方法(または self を使わないオブジェクト指向プログラミング)

はじめに

Lua でもオブジェクト指向プログラミングは可能です。しかし、普段 C++ や C# を使っているプログラマからすると、メンバ変数やメソッドにアクセスするために毎回 self と書かなければならないのは勘弁してほしいところです。また、コロンとピリオドの使い分けが必要な点にも注意を要します。本文書では、C++ や C# のような書き方を Lua でも実現する方法を紹介します。

この文書の目的

Lua でオブジェクト指向プログラミングを行う際に、self を使わない方法を紹介します。具体的には、以下のような書き方を実現する方法についてです。

  1. コロンによるシンタックスシュガーを使用しない
  2. メソッド定義時に、自分自身を指す self を引数にとらない
  3. メソッドでは、単に変数名を書くだけで同名のメンバ変数にアクセスできるようにする
  4. メソッド名についても、同様にメソッド名だけでアクセスできるようにする
  5. メソッドへのアクセスには . (ピリオド)を使うこととする

これらは self という考え方を必要としないため、自然に実現されます(もちろん、あえて self と書きたい方のために、self が該当のインスタンスを指すようにすることも可能です)。

なお、本文章では、継承については取り扱いません。

しくみ

self を明示しなければならないのは、単に変数名を記述するとグローバル変数を指してしまうからです。メソッド中に self に格納されたローカル変数と同名の変数がある場合、ローカル変数を指し示すことができれば、この問題は解決します。

関数の定義後でも、関数内に記述されたグローバル変数をローカル変数としてアクセスするように変更することができます。詳細は以前書いた「Lua で、定義した関数中のグローバル変数を、あとからローカル変数に差し替える方法」に譲りますが、関数が実行される環境を self が示すものにすることにできれば、上で示した 1 から 5 までの条件を満たすことができます。

具体的な方法

ここでは例としてスタックを定義してみます。Stack 型のクラスを定義し、それぞれのインスタンスには以下のメンバ変数があるものとします。

  • body : データを格納するリスト(実態はテーブル)
  • numOfElements : 現在格納されている要素の数

これらの変数は、Stack.new() の中で、newInstance テーブルの要素として定義されています。

また、Stack のインスタンスは、push、pop、dup というメソッドを持ちます(dup() はスタックトップにある値を複製するメソッドです)。それぞれのメソッドは Stack.push() 等として定義します。この時に、コロンによるシンタックスシュガーも必要としませんし、明示的にインスタンスを受け取る引数についても記述する必要はありません。

自分自身を示す self を使わないために、事前にこれらのメソッドのバイトコードを取得しておきます(例では Stack.bytecodeOfPush などとしています)。

インスタンスのメソッドとして、これらバイトコードの実行環境として newInstance を指定します。newInstance にはメンバ変数やメソッドが格納されています。これによりメンバ変数は、self.numOfElements などと書かなくても、単に numOfElements と書けば、それぞれのインスタンスに格納された numOfElements を指すようになります。

実際のコードを以下に示します:

no_self_Stack.lua
Stack={}

-- インスタンスメソッド
Stack.push=function(inValue) -- self が引数に無い
    -- self.body ではなく、単に body と書けば十分
    table.insert(body,inValue)
    -- self.numOfElements ではなく、ここも numOfElements で良い
    numOfElements=numOfElements+1
end
Stack.pop=function() -- self が引数に無い
    if numOfElements<=0 then
        error("stack is empty.")
    end
    numOfElements=numOfElements-1
    return table.remove(body) 
end
Stack.dup=function() -- dup() も同様
    local t=pop() -- self.pop() と書かなくてもよい
    push(t) -- これも self.push(t) と書かなくてもよい
    push(t)
end

-- バイトコードを取得
Stack.bytecodeOfPush=string.dump(Stack.push)
Stack.bytecodeOfPop=string.dump(Stack.pop)
Stack.bytecodeOfDup=string.dump(Stack.dup)

-- ユーティリティ関数
function buildInstanceMethod(inInstance,inBytecodeOfMethod)
    return load(inBytecodeOfMethod,nil,"b",inInstance)
end

Stack.new=function()
    local newInstance={}
    newInstance.body={} -- メンバ変数として body を定義
    newInstance.numOfElements=0 -- メンバ変数 numOfElements を定義

    -- newInstance に含まれていないものは _ENV を参照
    -- つまり、newInstance を優先的に参照するように設定
    setmetatable(newInstance,{__index=_ENV})

    -- メソッド割当て
    newInstance.push=buildInstanceMethod(newInstance,
                                         Stack.bytecodeOfPush)
    newInstance.pop =buildInstanceMethod(newInstance,
                                         Stack.bytecodeOfPop)
    newInstance.dup =buildInstanceMethod(newInstance,
                                         Stack.bytecodeOfDup)

    return newInstance
end

次に、上で定義した Stack の使い方です。Lua に用意されている : (コロン)によるシンタックスシュガーは使用しませんので注意して下さい。

ex-Stack.txt
> stack=Stack.new() -- インスタンス生成
> stack.push(10)
> stack.numOfElements
1
> stack.push(20)
> stack.push(30)
> stack.numOfElements -- 現在のスタックの状態は bottom [10,20,30] top
3
> stack.dup() -- bottom [10,20,30,30] top
> stack.numOfElements
4
> stack.pop()
30
> stack.pop()
30
> stack.numOfElements
2
> stack.pop()
20
> stack.pop()
10
> stack.pop() -- 現在、スタックにはデータは格納されていないので、エラー
noSelf.lua:31: stack is empty.
stack traceback:
    [C]: in function 'error'
    noSelf.lua:31: in function <noSelf.lua:29>
    (...tail calls...)
    [C]: in ?
> stack.numOfElements
0

まとめ

関数の環境を差し替える技法を応用することにより、Lua でも C++ や C# のようなメソッドの記述を可能としました。もちろん、Stack の例では、numOfElements はインスタンスのメンバ変数を指すようになりますので、もしグローバル変数の numOfElements にアクセスしたい場合は、_G.numOfElements と書くような工夫が必要となります。

ここでは、self を使わないことを第一の目的としてサンプルコードを作成しました。そのため、全てが public なメンバとして公開されています。これは、故意もしくは意図しない操作により、メンバ変数の値やメソッドを上書きできます。もし、これらを適切に保護するのであれば、別途工夫が必要となります。これについては、また別の文章で書くことができれば、と思っています。