読者です 読者をやめる 読者になる 読者になる

Minecraftとタートルと僕

PCゲームMinecraftのMOD「ComputerCraft」の情報を集めたニッチなブログです。

Luaでオブジェクト指向(1)―基本はコロン記法とメタテーブル

はじめに

Luaは組み込み用途の小規模な言語であるにも関わらず、オブジェクト指向プログラミングが可能です。なにそれ怖い。

Luaはインスタンスベース*1のオブジェクト指向プログラミングをサポートしています。

もっと具体的に言うならば、オブジェクトの雛形としてクラス設計を重視する(堅苦しい)クラスベースのオブジェクト指向ではなく、
元となるインスタンス(プロトタイプ)をベースに、自由にインスタンス変数・メソッドを追加しつつ柔軟に新しいオブジェクトを作成できることが特徴です。

インスタンスベース・オブジェクト指向の利点は後からの変更に対する許容性の高さです。クラスベースの方は、かっちり決まればとてもきれいなプログラムができますが、後からの変更を考えるとクラスまで遡って変更を加えるのはきついよねというある意味妥協の産物とも言えますw

一昔前はクラスベースを採用しているプログラム言語を多く見かけましたが、
最近は、取り回しの軽さを重視してプロトタイプベースのオブジェクト指向を採用している言語が注目を浴びているようです*2

そしてLuaでこのような機能が利用できるのは、だいたいはテーブルのおかげ。テーブルの力ってすげぇ。
ちょっとだけ試してみましょう。

座標オブジェクトを作る

-- ################# define function
-- オブジェクトを作成するnewメソッドを定義(Javascriptで言うコンストラクタのようなもの)
Coordinate = {}
Coordinate.new = function(_x,_y)
  local obj = {}
  obj.x = _x
  obj.y = _y

  -- pp というインスタンスメソッドをオブジェクト内に格納
  obj.pp = function(self) print(string.format("(%d,%d)", self.x, self.y)) end

  return obj 
end

-- ################## main
point1 = Coordinate.new(1,3) -- 座標(1,3)というオブジェクト
point2 = Coordinate.new(3,2) -- 座標(3,2)というオブジェクト

point1.pp(point1)
point2.pp(point2)

このプログラムの実行結果として、オブジェクトpoint1とpoint2の内容を表示します。

(1,3)
(3,2)

ポイントは、クラスを定義するのではなく、インスタンスを作成するnewメソッドをいきなり定義するところです(「Javascriptのコンストラクタ」ですね。まさしく)。
これで座標オブジェクトを、new()メソッドを使うことでいくらでも作れるようになりました。

ただし、このやり方だと2つほど不満があります。

不満1:オブジェクトのメソッドを利用するときに表記が冗長

まず一つ目の不満は、「point1.pp(point1)」のように「point1」を2回も繰り返して記述していることです。冗長ですね。

実はこの表記は以下のように「:」(コロン記法)を使うと自動的に変換してくれます。

-- 以下の書き方は、
point1.pp(point1)

-- 以下と同等です
point1:pp()

今後はコロン記法を使いましょう。


不満2:オブジェクトごとに新しくメソッド作ってますよね

さきほど作った2つのオブジェクトのメソッド「pp」のIDを調べてみましょう。

point1 = Coordinate.new(1,3) -- 座標(1,3)というオブジェクト
point2 = Coordinate.new(3,2) -- 座標(3,2)というオブジェクト

print(point1.pp)
print(point2.pp)

その実行結果。

-- 以下、実際の数値は環境によって異なります。
function: 54238342
function: 3234b342

つまりオブジェクトごとに、全く同じ機能のメソッドを個別に作って格納しています。
今はオブジェクトが2個だから大した問題ではないですが、オブジェクトを100個、1000個と作ったら、同じ機能のメソッドをわざわざ100個、1000個と作ることになるんです。これ、すごく無駄ですよね。

全く同じメソッドなのですから、一回メソッドを定義したら、各オブジェクトはそれを参照するようにしたいですよね。
そこで出てくるのがLuaのもう一つの秘密兵器、メタテーブルです。

メタテーブルとは

Luaのメタテーブルを使うと、様々なデータ型に対して新しい操作(演算)を定義できます。
Lua5.1では、テーブル(とユーザーデータ)だけが、個々で独自のメタテーブルを持ちます。それ以外のデータ型は、その型すべてで1つのメタテーブルを共有することになっています。

メタテーブルを使って新しい演算子を定義

以下の例では、メタテーブルを使って、テーブルに対して新しい操作(テーブルの連結)を定義しています。

-- まず、新規作成されたテーブルにはメタテーブルは設定されていません。
t1 = {}
print(getmetatable(t1))  --> nil

それではテーブルに対して新しい演算子(今回は「..」)を定義しましょう。

-- テーブル「mt」の中に、キー「__concat」を使ってメタメソッドを格納
mt = {}
mt.__concat = function(x,y)
  local results = {}
  for i,_ in ipairs(x) do
    table.insert(results, x[i])
  end
  for i,_ in ipairs(y) do
    table.insert(results, y[i])
  end
  
  return results
end

-- テーブルt1、t2のメタテーブルにmtをセットする
t1 = {1,2,3}
t2 = {4,5,6}
setmetatable(t1,mt)
setmetatable(t2,mt)

-- 連結演算子は「..」
t3 = t1..t2
 --> {1,2,3,4,5,6}

メタテーブルに設定できるキー(Luaではイベントと呼びます)は数多くの種類があります。
上記では、イベント「__concat」にメタメソッドを設定することで連結演算子「..」を定義しました。他にもイベント「__sub」で「-」演算子、イベント「__add」で「+」演算子などが新しく定義できます。

詳しくはLuaリファレンスを見ていただくことにして、今回のオブジェクト指向プログラミングでは、参照演算子「__index」に注目してみましょう。

参照演算子「__index」とは

テーブルにおいて、未定義のキーを参照しようとすると「nil」が返ってきます。

t = {}
print(t.hevo) --> nil

しかし、メタテーブルに参照演算子「__index」を設定することで、未定義のときにデフォルトで探しに行くテーブルを設定できます。

-- デフォルト参照用のテーブル
t0 = {hevo=1}

-- テーブル「mt」作成。キー未定義時の参照テーブルとしてt0を指定
mt = {__index = t0}

-- テーブル「t1」のメタテーブルにテーブル「mt」をセット
t1 = {}
setmetatable(t1, mt)

-- テーブル「t1」にはキー「hevo」がないので「t0」から探してきた
t1.hevo --> 1

このように「__index」は、テーブルの未定義キーだったら他のテーブルから探してくるという参照操作を定義できるわけです。
これは、オブジェクト指向における継承を実現するの使えます。

座標オブジェクト作成再び(コロン記法とメタテーブルの利用)

-- ################# define object
-- プロトタイプを定義
Coordinate = {}
Coordinate.pp = function(self) print(string.format("(%d,%d)", self.x, self.y)) end

-- オブジェクトを作成するnewメソッドを定義(Javascriptで言うコンストラクタのようなもの)
Coordinate.new = function(_x,_y)
  local obj = {}
  obj.x = _x
  obj.y = _y
  -- 未定義だったらテーブル「Coordinate」を参照するようセット
  setmetatable(obj,{__index = Coordinate})

  return obj 
end -- ミスのご指摘がありこの行を修正(2015.11.15)

-- ################## main
point1 = Coordinate.new(1,3) -- 座標(1,3)というオブジェクト
point2 = Coordinate.new(3,2) -- 座標(3,2)というオブジェクト

-- 座標を表示
point1:pp()
point2:pp()

-- メソッド「pp」のIDを表示
print(Cordinate.pp)
print(point1.pp)
print(point2.pp)

表示結果は次のようになります。

(1,3)
(3,2)
function: 135bd509
function: 135bd509
function: 135bd509

コロン記法を使うことで、メソッド実行をシンプルに表記できますね。
また、メタテーブルの「__index」イベントを使うことで、プロトタイプ(別のインスタンス)のメソッドを参照するようにできました。
つまりこれは、クラスベース・オブジェクト指向で言う、クラスメソッドですね。

まとめ

  • Luaでオブジェクト指向プログラミングが可能だよ。しかもプロトタイプベース、インスタンスベースなので、個々のインスタンスに対して自由に変数・メソッドを設定できる。
    • テーブルの力ってすげぇ!
    • 普段Javaを使い慣れている人はとまどうかもね。Javascript使ったことあるなら大丈夫だけど。
  • クラスを直接定義するのではなく、オブジェクトを作成するnewメソッドを定義する。
    • newメソッド内で定義した変数・メソッドは、インスタンス変数・インスタンスメソッドとなる。
  • メソッドやプロパティにアクセスするためには、コロン記法を利用すると便利。
  • Luaのメタテーブルを使うことで、他のオブジェクトの変数・メソッドをまとめて参照できる。

*1:私はプロトタイプベースという難しい響きが苦手なのでこちらの用語を好んで使います

*2:とはいえ、私はRubyというプログラム言語も好んでよく使っています。クラスベースであるにも関わらずクラスを柔軟に取り扱うために様々な機能を取り入れているところが気に入っています。