LjES のオブジェクトの仕組み (2014/05/17)
今回は Raspberry Piに限らない、Lua または LuaJIT のオブジェクト指向プログラミングに関するものです。Lua でオブジェクト指向する方法はいくつか (Programming in Lua / 16-Object-Oriented Programming やhttps://lua-users.org/wiki/SampleCode など)ありますが、ここでは LjES で使っている方法を紹介します。以前、Lua の文法について の最後にちょっと LjES のオブジェクトモデル ということで書きましたが、仕組みの細かいところを把握するのは大変なので、実際に Lua を動かしながらゆっくり確認していきましょう。
オブジェクト指向の概要
LjES のように3次元の世界を扱う場合、たくさんの「物」が同時に存在することになります。大きさ、色、位置、角度、速度などの性質が異なった「物」をプログラムで扱うには、データと処理をまとめて管理するオブジェクト指向というプログラミング手法が便利です。「物」を数値や文字と同じように1つのデータ型として扱えると、プログラムの見通しが良くなります。LjES が使っている言語 Lua には文法的に直接オブジェクト指向プログラミングを行う構文はありませんが、テーブルを利用して「オブジェクト」を実現することができます。
Lua または LuaJIT さえ動作すれば、どんなOSでも今回のLua プログラムの動作は確認できます。
用語の使い方
まず最初にオブジェクト指向プログラミングの用語は言語ごとに微妙に表現が異なるため、この記事で使う用語の解説をしておきます。
Lua では JavaScript と同じ種類の「プロトタイプベース」とか「オブジェクトベース」と呼ばれる形式でオブジェクト指向プログラミングを行います。空のオブジェクトを生成して、メソッドや変数をオブジェクトに追加するコードを実行することでオブジェクトを組み立てたり、既存のオブジェクトをコピーしてから、修正して新しいオブジェクトを生成する方法となります。 素直に考えると自然な方法に思えますが、クラスが動的に変化せず、静的に定義される C++、Java といったクラスベースのオブジェクト指向言語に慣れていると分かり難いかもしれません。
クラスとインスタンス
C++ のようなクラスベースのオブジェクト指向言語では、クラスはオブジェクトを作るための設計図で、それをもとに作られた個々の実体 (オブジェクト)のことをインスタンスと呼びます。「このオブジェクトは、クラスAのインスタンスです。」のようにオブジェクトの種類を示します。
一方、プロトタイプベースの言語では、オブジェクトは空のオブジェクトをもとに組み立てたり、別のオブジェクトをコピーして生成するため、クラスとインスタンスといった明確な区別はありません。
LjES では設計図としてのクラスに相当するオブジェクトと、クラスから作成される個別の実体(インスタンス)としてのオブジェクトを分けて考えることにします。
クラスとして使用するオブジェクトはクラス名として英大文字ではじめます。また、インスタンスとして使用するオブジェクト名は小文字ではじめます。原則として、クラスに相当するオブジェクトを「クラス」、クラスから作られたインスタンスのことをオブジェクトと呼ぶことにします。
例: -- クラスからオブジェクトを作成 local instance = ClassName:new() local eye = Node:new(nil, "Eye")
クラスを基にオブジェクトを生成する場合は上のように書きます。LjES では、関数名が new のメソッドをオブジェクトを生成して初期化するコンストラクタとして使っています。
メソッドとプロパティ
この文書では、関数が入っているクラスに属する変数をメソッド、データが入っているクラスに属する変数をプロパティと呼ぶことにします。
Lua ではテーブルを使ってオブジェクトを実現しますが、クラスに属する関数は、クラスに属するデータと同じくテーブルの要素です。下の3つの書き方はどれも同じメソッドを定義しています。
(1) function Class.method(self) print(self.name) end (2) Class["method"] = function(self) print(self.name) end (3) Class.method = function(self) print(self.name) end
どの書き方でもクラスの変数であるプロパティに関数定義を代入しているだけです。Class というテーブルをクラス用のオブジェクトとして使って、"method" という文字列をキーとした要素に関数定義を代入しています。
Class.property = 1 Class["property"] = 2
データを格納するプロパティについても、上のどちらの書き方でも同じで、Class というテーブルの "property" という文字列をキーとした要素に値を代入しています。
LjES ではメソッドもプロパティも以下の書き方に統一しています。
function Class.method(self) print(self.name) end Class.property = 1
LjES のオブジェクト指向の実現方法
Lua ではテーブルをクラスまたはクラスのインスタンス(オブジェクト)として使うことで、オブジェクト指向として必要な機能を実現します。同じ型のデータ (オブジェクト) をたくさん使う場合でも、なるべくメモリを無駄にしないようにインスタンスの共通部分をくくり出したり、既存のクラスとは少し異なる派生クラスを「継承 (inheritance)」を使って実現しています。
Lua のテーブル
Lua には テーブル というデータ形式があります。テーブル とは「配列の添え字に文字列が使えるもの」です。これは 「JavaScript のオブジェクト」、「PHP の配列」、「Ruby のハッシュ」、「Perl の連想配列」、「Python の辞書」というデータ形式とほぼ同じものです。言語ごとに色々な理由で異なる名前で呼ばれていますが、実体は、「ハッシュ法(hashing)」と呼ばれるデータ検索アルゴリズムの一種を使ったハッシュテーブルとして実現されています。内部的には文字列の文字コードをハッシュ関数と呼ばれる演算で整数に変え、この整数が示す配列の位置にデータを格納します。ハッシュテーブルというデータ構造を使うと、高速に要素の挿入/削除/検索ができるため、どの言語でも重要なデータ構造として採用されています。Lua のテーブル は JavaScript のオブジェクトと非常に近くて、オブジェクト指向プログラミングを実現するうえで重要なデータ形式です。
ハッシュテーブルという仕組みを使ったデータ形式を「オブジェクト」と呼ぶか、「テーブル」と呼ぶかということだけです。 「テーブル」と呼ぶとリレーショナルデータベースの用語とまぎらわしいのですが、これは慣れの問題です。慣れてください。
Lua のテーブル は、テーブルに対してメタテーブル を設定することで、テーブルに対する操作を制御する機能を持っています。 テーブルの要素を検索してもキーが見つからなかった場合に、メタテーブルの __index 要素にテーブルが設定されていると、そのテーブルを検索対象として追加できます。 そのテーブルでも見つからないと、そのテーブルのメタテーブルの __index を見るといった動作となります。この機能を使うことで何階層も継承元をさかのぼってアクセスすることが可能となります。 ここではメタテーブルの詳細は気にしなくても大丈夫です。
オブジェクトの実体
テーブルをオブジェクトとして使う仕組みを、テーブルに格納されている要素の実体を見ながら確認していきます。LjESではすべてのクラスとインスタンスの元となる Object という名のクラスを使います。クラスとして使うオブジェクトになるため、大文字で始まる名前にしています。 最初に Object 自身には空のテーブルを代入します。
Object = {}
次の図は空のテーブルを示します。 欄外にはテーブル名、左側の欄はキー、右側には格納されているデータ内容を示すものとします。
Object +---------+------------------+ | | | +---------+------------------+
次に Object.new というメソッドを、以下の内容の関数として定義します。Object の new メソッドは新しい空のテーブルを生成 (local obj = {}) して、ローカル変数 obj に代入します。 空テーブルが入った obj に、自分自身をメタテーブルとして設定 (setmetatable(obj, obj)) します。これでメタテーブル用のテーブルを別に用意する必要がなくなります。また、__index キーに対して継承元の Object テーブルを設定します。ここでは、まだ new 関数を定義しただけであることに注意してください。
function Object.new(self) local obj = {} setmetatable(obj, obj) obj.__index = self return obj end
Object に空のテーブルを代入する「Object = {}」と上の関数が、LjESのクラスの基になっている Object.lua のすべてです。この後の文章は、なぜこれでオブジェクト指向が実現できるのかという説明になります。
この new 関数を定義した後では Object テーブルの内容は以下のようになります。
Object +---------+------------------+ | new | function ... end | +---------+------------------+
Object クラスのインスタンスとして aObject オブジェクトを作成してみます。インスタンスとしてのオブジェクトのため、aObject と小文字で始まる名前にしています。 Object:new() の返り値を aObject に代入するとObjectクラスのインスタンスとなります。 ここで aObject の内容を考えてみます。
aObject = Object:new()
ここで初めて Object:new() を実行されました。aObject の内容は Object:new() を実行して返されたオブジェクトです。
aObject +---------+------------------+ | __index | Object | +---------+------------------+
ここで Object:new() は Object.new(Object) の省略記法で、コロンの前のオブジェクトが先頭の引数として渡されます。 new の定義側ではfunction Object.new(self) としているので self にコロンの前のオブジェクトが渡されることになります。
さて、 「aObject = Object:new()」を実行すると、以下のように動作します。
- local obj = {} の行で obj という名称の空のテーブルを作成。
- setmetatable(obj, obj) で obj テーブルのメタテーブルを obj テーブル自身に設定します。
- メタテーブル(obj テーブル) に __index キーに引数の self で渡された Object オブジェクト(実体はテーブル)を設定します。
- この obj が Object:new() から返って、aObject に代入されます。
こうして、aObject はObjectクラスのインスタンスとして動作します。aObject 自身はプロパティもメソッドも持っていませんが、アクセスされると__index に設定されたテーブル (この場合はObject) を検索して、Object にあるプロパティやメソッドを(もしあれば)使うことができます。
この例では Object は new メソッドしか持っていないので Object のインスタンス(Object オブジェクト)のままでは何もできません。
ところでここで aObject:new() を呼び出したら何が起こるのでしょうか?
bObject = aObject:new()
bObject +---------+------------------+ | __index | aObject | +---------+------------------+
aObject:new() つまり aObject.new(aObject) はメソッドを __index から検索した結果として見つかった Object.new(aObject) が実行されるため、bObject は上に示すようにプロパティやメソッドを aObject を探しに行くオブジェクトとなります。
さて、もう一度同じことを実行してみましょう。
aObject = Object:new()
aObject +---------+------------------+ | __index | Object | +---------+------------------+
ここで aObject にも new メソッドを定義してみます。
function aObject.new(self, name) obj = Object.new(self) obj.name = name return obj end
aObject テーブルは次のように "new" をキー、関数定義をデータとした項目が増えます。
aObject +---------+------------------+ | __index | Object | +---------+------------------+ | new | function ... end | +---------+------------------+
ここで aaObject = aObject:new("A") を実行すると変数 name をプロパティとして持つ、次のような中身のオブジェクトができます。
aaObject +---------+------------------+ | __index | aObject | +---------+------------------+ | name | "A" | +---------+------------------+
さらに baObject = aObject:new("B") を実行すると次のような中身のオブジェクトができます。
baObject +---------+------------------+ | __index | aObject | +---------+------------------+ | name | "B" | +---------+------------------+
aObject を上位に持つ2つのオブジェクト、aaObject と baObject ができました。それぞれ別の値を持つプロパティとして name を持ち、上位のオブジェクト aObjectを知っています。aaObject:new() は __index を経由して aObject:new() が実行されます。
コード
以上の内容を実際に実行して確認してみます。 上で行った操作を実際に実行するためのプログラムになっていますが、オブジェクトの内容を表示するための checkTable 関数とプリント関数を追加しています。次のテキストを object.lua というファイル名で保存します。
function checkTable(Table) local nameList = {} for name, value in pairs(Table) do nameList[#nameList + 1] = name end table.sort(nameList) for i = 1, #nameList do print(string.format("%10s : %s", nameList[i], Table[nameList[i]])) end print() end Object = {} function Object.new(self) local obj = {} setmetatable(obj, obj) obj.__index = self return obj end print("Object -- ", Object); checkTable(Object) aObject = Object:new() print("aObject -- ", aObject); checkTable(aObject) bObject = aObject:new() print("bObject -- ", bObject); checkTable(bObject) function aObject.new(self, name) obj = Object.new(self) obj.name = name return obj end print("aObject -- ", aObject); checkTable(aObject) aaObject = aObject:new("A") print("aaObject -- ", aaObject); checkTable(aaObject) baObject = aObject:new("B") print("baObject -- ", baObject); checkTable(baObject)
実行結果
Lua または LuaJITで実際に実行すると以下のように出力されます。テーブルは 0xXXXXXXXX の16進形式で表示されますが、テーブルのメモリアドレスのため環境によって異なる値になります。
例えば aObject の __index の値が、Object の値と一致しているため、aObject の __index に Object (への参照) が格納されていることが確認できます。
$ luajit object.lua
Object -- table: 0xb6cf5fd0
new : function: 0xb6cf5ff8
aObject -- table: 0xb6cefa30
__index : table: 0xb6cf5fd0
bObject -- table: 0xb6cf6530
__index : table: 0xb6cefa30
aObject -- table: 0xb6cefa30
__index : table: 0xb6cf5fd0
new : function: 0xb6cf4f18
aaObject -- table: 0xb6cf66e8
__index : table: 0xb6cefa30
name : A
baObject -- table: 0xb6cf67e0
__index : table: 0xb6cefa30
name : B
aaObject と baObject は aObject クラスの2つのインスタンスとしてふるまうオブジェクトになります。
クラスの継承
ここで、aObject を AObject と書き換えてみます。この形式は LjES におけるクラスの継承の典型的なパターンとなっています。
AObject = Object:new() function AObject.new(self, name) obj = Object.new(self) obj.name = name return obj end
AObject +---------+------------------+ | __index | Object | +---------+------------------+ | new | function ... end | +---------+------------------+
同じように AObject クラスを継承する BObjectクラスを考えます。AObject クラスにプロパティの type とメソッドの print を追加します。
BObject = AObject:new(nil) function BObject.new(self, name, type) obj = AObject.new(self, name) obj.type = type return obj end function BObject.print(self) print(self.name, self.type) end
BObject +---------+------------------+ | __index | AObject | +---------+------------------+ | new | function ... end | +---------+------------------+ | print | function ... end | +---------+------------------+
BObject 自身はメソッドのコードを保持しています。次に BObject クラスのオブジェクトを作ります。
aBObject = BObject:new("B", "b-type")
BObject クラスのインスタンスである aBObject はプロパティを持ちますが、 メソッドは __index を通して BObject のオブジェクトが持つメソッドが実行されます。 こうして、クラスのオブジェクトはメソッドを持ち、インスタンスはプロパティだけを 持つことで、メモリを節約することができます。
aBObject +---------+------------------+ | __index | BObject | +---------+------------------+ | name | "B" | +---------+------------------+ | type | "b-type" | +---------+------------------+
継承を確認するコード
先ほどと同じように、実際に実行して確認してみます。上で行った操作を実際に実行するためのプログラムになっていますが、各オブジェクトの内容を表示する部分を後半に追加しています。 このプログラムでは各オブジェクトが上位階層のオブジェクトを参照している場合はそれも再帰的に表示します。
Object = {} function Object.new(self) local obj = {} setmetatable(obj, obj) obj.__index = self return obj end AObject = Object:new() function AObject.new(self, name) obj = Object.new(self) obj.name = name return obj end BObject = AObject:new("newA") function BObject.new(self, name, type) obj = AObject.new(self, name) obj.type = type return obj end function BObject.print(self) print(self.name, self.type) end aBObject = BObject:new("newB", "BObject") aBObject:print() -- ここから下はオブジェクトの内容を確認するコード -- テーブルの内容を表示する関数 function checkTable(Table) local nameList = {} for name, value in pairs(Table) do nameList[#nameList + 1] = name end table.sort(nameList) for i = 1, #nameList do print(string.format("%10s : %s", nameList[i], Table[nameList[i]])) end print() local mt = getmetatable(Table) if mt ~= nil then local next = mt.__index if next ~= nil then checkTable(next) end end end print("Object -- ", Object) checkTable(Object) print("AObject -- ", AObject) checkTable(AObject) print("BObject -- ", BObject) checkTable(BObject) print("aBObject --", aBObject) checkTable(aBObject)
実行結果
16進数で読みにくいですが、例えば aBObject は __index に BObject が設定されていて、さらに AObject、Object と継承元のクラスが順にリンクされていることが確認できます。階層をすべて表示しているので、どのオブジェクトも最後には 「new : function: 0xb6d06de8」だけの行、つまり Object (table: 0xb6d0c070) を継承していることが確認できます。
$ luajit inheritance.lua
newB BObject
Object -- table: 0xb6d0c070
new : function: 0xb6d06de8
AObject -- table: 0xb6d0c0d0
__index : table: 0xb6d0c070
new : function: 0xb6d0cc58
new : function: 0xb6d06de8
BObject -- table: 0xb6d0cc70
__index : table: 0xb6d0c0d0
name : newA
new : function: 0xb6d0ccd0
print : function: 0xb6d0cd50
__index : table: 0xb6d0c070
new : function: 0xb6d0cc58
new : function: 0xb6d06de8
aBObject -- table: 0xb6d0cd68
__index : table: 0xb6d0cc70
name : newB
type : BObject
__index : table: 0xb6d0c0d0
name : newA
new : function: 0xb6d0ccd0
print : function: 0xb6d0cd50
__index : table: 0xb6d0c070
new : function: 0xb6d0cc58
new : function: 0xb6d06de8
LjES のオブジェクトモデルの使い方
LjES で使っているクラスの継承の方法を一般化すると次のようになります。太字になっている5行のパターンが重要です。
-- (1) -- 新クラス = 継承元クラス:new() -- (2) -- function 新クラス.new(self, 引数) local obj = 継承元クラス.new(self) obj.変更プロパティ = 新初期値 obj.追加プロパティ = 引数 return obj end -- (3) -- function 新クラス.追加メソッド(self, 引数) 何か処理 end -- (4) -- local オブジェクト = 新クラス:new(引数)
以上の疑似コードを順に見ていきましょう。
まず (1) では継承元のクラスのオブジェクトが新クラスとなるオブジェクトに入ります。この時点では新クラスのオブジェクトは継承元のクラスのインスタンスです。新クラスのオブジェクトの内容は継承元のクラスのインスタンスそのものです。
次に (2) では 継承先クラスのテーブルに "new" というキーの要素となる関数が代入されます。new メソッドの内部では継承元クラスのオブジェクトを生成して、新しいプロパティを追加したり、継承元クラスで初期化されたプロパティの値を変更します。新クラスのオブジェクトは、継承元クラスのオブジェクトとは異なる新クラスのオブジェクトとなります。
新しいクラスにメソッドを追加する場合は (3) のように新クラスに新たなメソッドを定義します。
(4) では、新クラス:new(引数) が実際に実行された時点で継承元クラスの new が内部的に実行されて継承元クラスが持っていたプロパティが設定され、さらに 継承元クラスから順次最上位の Object へのリンクが設定されることで上位のクラスのプロパティ、メソッドがアクセスできるようになります。
実際に実行できるコードを示します。
-- いつもの Object Object = {} function Object.new(self) local obj = {} setmetatable(obj, obj) obj.__index = self return obj end -- BaseClass は Objectを継承 -- プロパティもメソッドも追加なし BaseClass = Object:new() -- ここから BaseClass を継承した NewClass を定義 NewClass = BaseClass:new() function NewClass.new(self, arg) local obj = BaseClass.new(self) obj.property0 = 1 obj.new_property = arg return obj end function NewClass.newMethod(self, arg) print('new method') end -- NewClass のオブジェクトを作って実行 local anObject = NewClass:new(10) anObject:newMethod()
一応実行してみます。エラーのないことを確認するだけですが。
$ luajit sample.lua
new method
LjES のクラスファイルの使い方
LjES では大文字で始まるファイル名はクラスの定義になっていて、ファイル名から拡張子を除いたクラス名のオブジェクトをグローバル変数として生成しています。 LjES の各モジュールの先頭付近に new メソッドがあります。実際に ソースコード(https://github.com/jun-mizutani/ljes) を確認してみてください。
例えば Node クラスを使う場合には、次のように記述できます。
require("Node") local node0 = Node:new(nil, "node") node0:rotateX(10)
Node というグローバル変数がクラスとして Node.lua 内で宣言されています。 先頭部分の require("Node") によって、Node.lua が読み込まれグローバル変数 Node にNodeクラスのオブジェクトが設定されます。 したがって require("Node") の後ろでは Node をクラス名として使うことができます。Node:new(nil, "node") で Node クラスのオブジェクトが生成されて返されます。 LjES 全体に渡って require("クラス名") のあとで クラス名:new() を実行すれば オブジェクトが生成できるようになっています。 返ったオブジェクトをローカル変数で受けて、変数名:メソッド() の形式で使います。 上の例では node0 オブジェクトは X軸周りに10度回転します。
今回紹介した方法では、プロパティやメソッドへの外部からのアクセス制限はできていません。「外からプロパティに直接書き込まないでね」というマナーが必要ですが、外からプロパティを操作するほうがメソッドを呼ぶより難しいので大丈夫ではないでしょうか。ここ1年ぐらいの間に20000行ほど上の方法のオブジェクトを使って LuaJIT のプログラミングしていましたが、「Object = {}」と「Object.new(self)」メソッドだけとコンパクトなオブジェクトモデルなので自分では気に入っています。