ぱろっと・すたじお

技術メモなどをまったりと / my site : http://parrot-studio.com/

Luaにおけるオブジェクト指向的記述の検討

最近、ちょいとLuaについていろいろ調べております...φ(・ω・`)


wikipedia:Lua


スマフォアプリの開発言語として使われたり、
サーバサイドの設定記述言語として使われたりと、
昨年あたりから観測範囲での事例が増えてきたので、ここらで触っておこうかと


大雑把な仕様はこちらを(´・ω・)っ


Lua: 5.0 ¥ê¥Õ¥¡¥ì¥ó¥¹¥Þ¥Ë¥å¥¢¥ë
八角研究所 : Series: 高速スクリプト言語「Lua」を始めよう! «
良いもの。悪いもの。: Lua基礎文法最速マスター
Atsushi's Homepage ` Lua ‚Å‘g‚ñ‚Å‚Ý‚é


Luaは他のアプリへの組み込みを意識しているので、非常に軽量で速い言語・・・だそうです
そのわりに言語仕様の汎用性が高く、
個人的にはRubyJavaScriptの軽量版的に感じました


もちろん、調べるだけでは意味がなくて、
実際に書いてみようと思ったわけですが、
私の身近にはとても都合の良い「題材」がありまして


http://ragnarokonline.gungho.jp/gameguide/system/pet/homunculus-ai.html


ROの一要素である「ホムンクルス」のAIはLua*1で書かれており、
実は2006年の実装時に、私も一通り調べていました


http://parrot.blog21.fc2.com/blog-category-12.html


ただですね・・・このときはあまり細かく見なかったのですが、
Luaを本格的に使おうといろいろ調べた結果、
デフォルトのAIのコードがかなり「汚い」と気づいたのですΣ(゚Д゚)ガーン


設計そのものは状態遷移モデルでわりと綺麗だったので、
自分でいろいろ修正するにあたり、
まず「設計はそのままで一から綺麗に書き直す」というのをやることに


そのコードはgithubで随時更新しております(´・ω・)っ


https://github.com/parrot-studio/ro-homu-ai


とまあ、例によって前置きが長くなりましたが、
Luaオブジェクト指向的なコードを書くにはどうするのか、
ちょっと検討してみましょう

クラス/インスタンス/new


まず前提として・・・

・・・というのがポイントです
最初の特徴はRubyJavaScriptに近いですが、
「this」がないというのが大きな特徴です*2 *3


ということで、Luaのクラス的な記述はこんな感じに(´・ω・)っ

Homu = {} -- ある種のクラス名
Homu.new = function(id) -- コンストラクタに相当
  local this = {} -- オブジェクトの本体 thisと書いちゃっても競合しない
  this.id = id    -- プロパティ
  this.hp = 100   -- これもプロパティ

  -- メソッドの定義
  -- thisにfunctionオブジェクトを格納する、という書き方
  -- 第一引数にselfを書くのがポイント
  this.cureHp = function(self, point)
    -- この「self」はインスタンス自身を指している
    self.hp = self.hp + point
  end

  this.useHealSkill = function(self)
    -- この呼び出し方がポイント
    -- Luaのシンタックスシュガーにより、それっぽいコードを実現している部分
    self:cureHp(100)

    -- 「self:cureHp(100)」という書き方は、第一引数にレシーバ自身を暗黙的に渡している
    -- 実際には「self.cureHp(self, 100)」の意味
  end

  -- selfを引数に取らないメソッド
  this.printDebug = function(str)
    -- インスタンスを参照しない
  end

  return this -- newの最後にメソッドやプロパティを構築したオブジェクトを返す
end

homuId = 10000
homu = Hoge.new(homuId) -- インスタンス生成

-- これも「homu.useHealSkill(homu)」に等しい
homu:useHealSkill()
homu.hp -- 200を返す

-- printDebugはselfを取らないので、「.」で呼び出さないとおかしなことに
homu.printDebug("回復したよ")


おそらく、Rubyにおけるクラスの仕組みや、
JavaScriptの"GoodParts"をご存じの方には、
なじみ深い方式なのではないでしょうか*4


ポイントはLuaにおけるシンタックスシュガーです
thisがないLuaでは、文法によって「それらしい表現」を実現しています


具体的には、「hoge:piyo(...)」というメソッド呼び出し*5をすると、
「hoge.piyo(hoge, ...)」のように、第一引数にオブジェクト自身を渡す、
という仕組みになってます


あとは、function側の第一引数に「オブジェクト自身」を意味するもの
(上ではRubyっぽく「self」)を定義すれば、
functionの中で「インスタンス」を参照できるわけです(`・ω・´) b


「これだけのこと」でそれっぽい表現を実現しているあたり、
Rubyによく似た思想に思えて面白かったので、
これを知ってLuaを真面目に書く気になりました

継承/override


インスタンスを作れるだけでもオブジェクト指向的ではありますが、
やはり「継承」がないと簡潔に書くことができません
とはいえ、さっきの仕組みの応用でいけるのですが

Yakitori = {} -- Homuを継承するつもりのクラス的オブジェクト
Yakitori.new = function(id)
  local this = Homu.new(id) -- Homeのインスタンスをthisに入れる

  -- override
  -- Homuが持つメソッドの中で、必要なもののみ上書きで再定義している
  this.useHealSkill = function(self)
    self:cureHp(200) -- このcureHpはHomuで定義されたものが呼ばれる
  end

  return this -- 継承したオブジェクトを返す
end

tori = Yakitori.new(homuId) -- 継承したオブジェクト
tori:useHealSkill() -- 200回復する
tori.hp -- 300を返す


ポイントは、継承するオブジェクトの実体(上ではthis)に、
「親のnewから取得したオブジェクト」を入れることです
(言葉にするとわけがわからないので、コードを見てください)


このオブジェクトに親の情報が全て詰まっているので、
子はその差分だけ書いてあげれば、
一般的なオブジェクト指向における「継承」が実現できますヽ(`・ω・´)ノ

super


しかし、先ほどのやり方には致命的な問題があります
親のfunctionを単純に「上書き」してしまうので、
このままでは親のfunctionを利用することができません(´-ω-)


Luaには「thisがない」=「superがない」ので、
それに相当する機能を、this同様に手動でやらないといけないわけです


とりあえず、最初に思いついたのはこんなやり方(´・ω・)っ

A = {} -- 親クラス(的なオブジェクト)
A.new = function()
  local this = {}

  this.hello = function(self, str)
    print("HELLO "..str)
  end

  return this
end

B = {} -- 子クラス(的なオブジェクト)
B.new = function()
  local this = A.new() -- 親クラスを継承

  this._hello = this.hello -- 親クラスのfunctionを別名で参照
  this.hello = function(self, str) -- 上書き
    self:_hello(str) -- 親クラスのhello
    print("hello "..str..'!!') -- 子クラスの実装
  end

  return this
end

a = A.new()
b = B.new()
a:hello('parrot') --> HELLO parrot
b:hello('parrot') --> HELLO parrot\nhello parrot!!
b:_hello('parrot') --> HELLO parrot

つまり、親が保持するfunctionオブジェクトを、
別な名前で参照できるようにしておき、
上書きしたfunctionの中で使う、というわけです


最初はこれでいいと思ったのですが、
「Bを継承したCでoverride」の時に困ると気づきました


もし、CでBと同じコードを書いたとすると、
Cが保存した「_hello」は「Bのhello」であって、
Aの情報が失われてしまうし、Bの中で循環参照が発生しますΣ(゚Д゚)ガーン


だからといって、「BがAを継承していること」をCが知らないといけない、
というのはやりたくないので、そこを暗黙的に処理する必要があります


そこで、いくつかルールを付け加えることにしました

  • 自身のメソッド名に「_」を使わない
  • overrideの際、元のfunctionを「元クラス名_function名」で保持する
    • overrideしたfunctionの中で、このfunctionをsuper的に使う


つまり、コードで書くとこういうことです(´・ω・)っ

B = {} -- 子クラス(的なオブジェクト)
B.new = function()
  local this = A.new() -- 親クラスを継承

  this.A_hello = this.hello -- 親クラスのfunctionを別名で参照
  this.hello = function(self, str) -- 上書き
    self:A_hello(str) -- 親クラスのhello
    print("hello "..str..'!!') -- 子クラスの実装
  end

  return this
end

C = {} -- 孫クラス(的なオブジェクト)
C.new = function()
  local this = B.new() -- BはAの子クラスだが、Aの存在をCは知らない

  this.B_hello = this.hello -- あくまでBのhelloを保存している
  this.hello = function(self, str) -- 上書き
    self:B_hello(str) -- Bのhelloを呼んだつもり(内部的にAも呼ばれているのだが)
    print("Good Bye "..str) -- 孫クラスの実装
  end

  return this
end

c = C.new()
c:hello('parrot') --> HELLO parrot\nhello parrot!!\nGood Bye parrot

この方法なら、「BがAを継承していること」をCが知らなくても、
ルールに従ってfunctionを保持しておくだけで、
多段階継承でのoverrideが実現できます( ゚д゚)o彡゚ *6




というわけで、Luaにおけるオブジェクト指向的なコーディングを検討してきましたが、
このやり方が妥当かは正直わかりません


もしLuaが一般的に使われるようになれば、
JavaScriptのように"GoodParts"が出てくるんでしょうね・・・

*1: バージョンは5.0で、table関連の関数が組み込まれていない

*2: ただまあ、JavaScriptの「this」は非常にややこしく、それだけのためにCoffeeScriptを書こうと思う人もいるわけで(´-ω-)

*3: そのCoffeeScriptのネタもあるのですが、これはまた次回・・・

*4: Rubyの"new"がただのメソッド呼び出しでしかない、というのは有名なお話

*5: 実際には「hogeというオブジェクトにpiyoという名前で格納されたfunctionオブジェクトの呼び出し」です

*6: これに近いことをRailsがやっていた・・・ような・・・