この文書の目的
Lua でもオブジェクト指向を実現することは周知の事実です。しかし、インスタンス生成のためのメソッド定義や、それぞれのインスタンスにどのようなメンバ変数やメソッドが備わっているのか、また、それらを適切に保護するコードを書くのは結構面倒な作業となります。
また、メソッドを記述する場合、メンバ変数にアクセスするためにいちいち self と書かなければならないもストレスがたまってしまいます。この文書では、以前書いた「self.name と書かなくても name だけでメンバ変数やメソッドにアクセスできるようにする方法」や、「メンバ変数やメソッド名を保護する」の応用として、Lua に class 文のようなものを導入してみます。
Lua では、関数の引数がテーブル 1 つであれば、文字列同様、関数呼び出しのカッコを省略することができます。今回はそれを活用し、オブジェクトの宣言っぽく書けるような Class 関数を作ってみます。
例として、X,Y,Z の要素を持つ 3 次元ベクトル型であれば、次のように定義できるようにします。
-- 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 クラスの使用例を以下に示します。
> 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=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 を用いて定義してみます。
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 言語の開発者は優秀だだなぁ、としみじみ思う次第です。