LoginSignup
5
6

More than 5 years have passed since last update.

Lua に class 文のようなものを導入する(self を使わないオブジェクト指向プログラミング一応完成編)

Last updated at Posted at 2017-08-26

この文書の目的

Lua でもオブジェクト指向を実現することは周知の事実です。しかし、インスタンス生成のためのメソッド定義や、それぞれのインスタンスにどのようなメンバ変数やメソッドが備わっているのか、また、それらを適切に保護するコードを書くのは結構面倒な作業となります。

また、メソッドを記述する場合、メンバ変数にアクセスするためにいちいち self と書かなければならないもストレスがたまってしまいます。この文書では、以前書いた「self.name と書かなくても name だけでメンバ変数やメソッドにアクセスできるようにする方法」や、「メンバ変数やメソッド名を保護する」の応用として、Lua に class 文のようなものを導入してみます。

Lua では、関数の引数がテーブル 1 つであれば、文字列同様、関数呼び出しのカッコを省略することができます。今回はそれを活用し、オブジェクトの宣言っぽく書けるような Class 関数を作ってみます。

例として、X,Y,Z の要素を持つ 3 次元ベクトル型であれば、次のように定義できるようにします。

Vector3.lua
-- Vector3.lua
-- example of Class

require("Class")

Vector3=Class {
    Public={ -- protected public
        X=0,
        Y=0,
        Z=0,

        Length=function()
            return math.sqrt(X*X+Y*Y+Z*Z)
        end,
    },

    -- operators
    Op={
        new=function(...)
            local numOfArgs=#{...}
            if numOfArgs==3 then
                X,Y,Z=...
            elseif numOfArgs~=0 then
                error("Vector3.new: invalid arguments.")
            end
        end,

        indexAccessRead=function(inIndex)
            return ({X,Y,Z})[inIndex]
        end,

        indexAccessWrite=function(inIndex,inValue)
            local t={X,Y,Z}
            t[inIndex]=inValue
            X,Y,Z=table.unpack(t)
        end,

        ["+"]=function(inV1,inV2)
            return Vector3.new(inV1.X+inV2.X,
                               inV1.Y+inV2.Y,
                               inV1.Z+inV2.Z)
        end,

        toString=function()
            return string.format("(X=%s, Y=%s, Z=%s)",X,Y,Z)
        end
    }
}

Vector3 クラスには、外部に公開された(しかし、外部から上書きすることはできない)メンバ変数の X,Y,Z とメソッド Length があり、演算子として、コンストラクタ、配列としてのアクセス(読み書き)、加算および文字列化が定義されています。

今回紹介する Class で定義した Vector3 クラスの使用例を以下に示します。

ex_Vector3.txt
> require("Vector3")
> v=Vector3.new(10,20,30)
> v
(X=10, Y=20, Z=30)
> v[1] -- 配列として v.X にアクセス(読取り) (注 1)
10
> v[2]=2 -- 配列として v.Y にアクセス(代入) (注 1)
> v
(X=10, Y=2, Z=30)
> u=Vector3.new(1,20,3)
> v+u
(X=11, Y=22, Z=33)

注 1 : インスタンスの各メンバ変数に、配列としてアクセスする場合については、「複数のメンバ変数を配列のように扱う方法」に書いておりますので、参考にして下さい。

なお、ここで定義する Class 関数でも継承は取り扱わないこととします。

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

Lua-5.3.4。メタテーブルが使える最近の Lua 5.3 系であれば、おそらく動作すると思います。

Class の実装例

少し長いですが、全てのソースを以下に記します。演算子用のメソッドを opBytecode テーブルに格納して、活用する部分がこれまでのものと異なる程度です。

演算子の定義に使用するキーワードですが、opMapper というテーブルの定義を読めば分かると思います。Lua の演算子とは異なるものに、コンストラクタと配列としてのアクセスがありますが、コンストラクタは演算子セクション(Op セクション)にて、new というキーワードで関数定義して下さい。インスタンスへの配列としてのアクセスですが、読取りは indexAccessRead キーワードに、書込み(代入)は indexAccessWrite にそれぞれ関数定義して下さい。

Class.lua
Class=function(inClassDef)
    local class={}
    local bytecode, opBytecode={},{}
    local private, writable={},{}
    local memberTemplate={}
    local opMapper={
        unaryMinus="__unm",
        ["+"]="__add", ["-"]="__sub",
        ["*"]="__mul", ["/"]="__div", ["//"]=idiv,
        ["%"]="__mod", ["^"]="__pow", [".."]=__concat,
        unaryNot="__bnot",
        ["&"]="__band", ["|"]="__bor",
        ["~"]="__bxor",
        ["<<"]="__bshl", [">>"]="__bshr",
        ["=="]="__eq", ["<"]="__lt", ["<="]="__le",
        toString="__tostring"
    }

    do
        local registerMember=function(inDef,inOptTable)
            for k,v in pairs(inDef) do
                if type(v)=="function" then
                    bytecode[k]=string.dump(v)
                else
                    memberTemplate[k]=v
                end
                if inOptTable then
                    inOptTable[k]=true
                end
            end
        end

        local registerOpMember=function(inDef)
            for k,v in pairs(inDef) do
                if type(v)~="function" then
                    error("index access op should be a function.")
                end
                if k~="indexAccessRead" and k~="indexAccessWrite"
                  and k~="new" and opMapper[k]==nil then
                    error("invalid operator name.")
                end
                opBytecode[k]=string.dump(v)
            end
        end

        local optTable={
            Private=private, Writable=writable, Public=nil,
        }
        local category
        local defs
        for category,defs in pairs(inClassDef) do
            if category=="Op" then
                registerOpMember(defs)
            else
                registerMember(defs,optTable[category])
            end
        end
    end

    class.new=function(...)
        local newInstance={}
        local member={}
        local k,v

        for k,v in pairs(memberTemplate) do
            member[k]=v
        end

        -- set default member variables.
        member.self=newInstance
        member.type=class

        local opMethod={}
        do
            setmetatable(member,{__index=_ENV})
            local buildInstanceMethod=function(inInstance,
                                               inBytecodeOfMethod)
                return load(inBytecodeOfMethod,nil,"b",inInstance)
            end

            local methodName
            local vmCode
            for methodName,vmCode in pairs(bytecode) do
                member[methodName]=buildInstanceMethod(member,vmCode)
            end
            for methodName,vmCode in pairs(opBytecode) do
                opMethod[methodName]=buildInstanceMethod(member,vmCode)
            end
        end

        member.__index=function(inTable,inKey)
            if private[inKey] then
                error(inKey .. " is a private member.")
            end
            if type(inKey)=="number" then
                if opMethod.indexAccessRead==nil then
                    error("there are no index access method.")
                end
                return opMethod.indexAccessRead(inKey)
            else
                return member[inKey]
            end
        end
        member.__newindex=function(inTabke,inKey,inValue)
            if writable[inKey] then
                member[inKey]=inValue
            elseif type(inKey)=="number" then
                if opMethod.indexAccessWrite==nil then
                    error("there are no index access method.")
                end
                opMethod.indexAccessWrite(inKey,inValue)
            else
                error(inKey .. " is not writable.")
            end
        end
        for k,v in pairs(opMethod) do
            if opMapper[k] then
                member[opMapper[k]]=v
            end
        end
        setmetatable(newInstance,member)

        if opBytecode.new~=nil then
            local info=debug.getinfo(opMethod.new)
            local nparams=info.nparams
            local numOfArgs=#{...}
            local errMessage="there are no appropriate constructors."
            if info.isvararg then
                if numOfArgs>=nparams then
                    opMethod.new(...)
                else
                    error(errMessage)
                end
            elseif numOfArgs==nparams then
                opMethod.new(...)
            else
                error(errMessage)
            end
        end

        return newInstance
    end
    return class
end

Class の使い方

上で示した Vector3 型も、そのまま Class のサンプルとしてすることができますが、private なメンバ変数や、自由に書込みできる public なメンバ変数を有していません。そこで、これまでにも紹介してきた Stack クラスを、Class を用いて定義してみます。

Stack.lua
require("Class")

Stack=Class {
    Private={
        -- テーブルはコンストラクタで生成するようにして下さい。
        -- ここで値を割り当てるとクラス変数となります
        -- (全ての Stack オブジェクトで共用することとなります)。
        body=nil,
        checkIndex=function(inIndex)
            if inIndex==0 or math.abs(inIndex)>NumOfElements then
                error("invalid index.")
            end
        end
    },

    Writable={
        Name="no name",
    },

    Public={
        NumOfElements=0,

        Push=function(inValue)
            table.insert(body,inValue)
            NumOfElements=NumOfElements+1
        end,

        Pop=function()
            if NumOfElements<=0 then
                error("stack is empty.")
            end
            NumOfElements=NumOfElements-1
            return table.remove(body)
        end,

        Dup=function()
            Push(self[1]) -- self も使える
        end
    },

    -- operators
    Op={
        -- stack=Stack.new(10,20,30)
        -- equivalent to:
        -- stack=Stack.new()
        -- stack.push(10); stack.push(20); stack.push(30)
        new=function(...)
            if #{...}>0 then
                body={...}
                NumOfElements=#body
            else
                body={}
            end
        end,
        indexAccessRead=function(inIndex)
            checkIndex(inIndex)
            local absoluteIndex=NumOfElements+1-inIndex
            if inIndex<0 then
                absoluteIndex=-inIndex
            end
            return body[absoluteIndex]
        end
    }
}

その他

Stack クラスの定義でも使用していますが、メソッド中で self も使うことができるようにしています。Stack.Dup() のように、自分自身に対する配列アクセスなどを実施したい場合などに使用することができます。また、型情報として type をデフォルトのメンバ変数として定義しています。型チェックなどを行いたい場合などに活用することができるかと思います。

まとめ

Lua が持つ、特定の条件下での関数呼び出しのためのカッコ省略という機能を活用して、class 文のように使える、Class 関数を定義してみました。できるだけ自然に書けるような仕様を目指しましたが、何か不明な点などがありましたら、Qiita のコメント機能などを活用していただければ幸いです。

余談

Class 関数なんぞを作っておいて言うことでは無いかもしれませんが、Lua でクラス宣言なんて…という気も。ただまぁ、無改造の Lua でも、ここまで可能なんだなぁ、と少々感慨深く思っております。

小型の言語にもかかわらず、こんなに面白いことができるなんて。Lua 言語の開発者は優秀だだなぁ、としみじみ思う次第です。

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6