Luaの文法とオブジェクト指向

言語慣れが足りなくて足踏みしています。
http://sugarpot.sakura.ne.jp/yuno/html/lua51_manual_ja.html
Lua5.1リファレンスマニュアルの勝手な日本語訳。オフィシャルの言語マニュアルなので信頼性が高いが、正確ゆえによくある使い方(やイディオム)やわかりやすさにちょっと難があるかも。BNFや各種言語のよくあるネタを知らないと全部は読みづらい。
http://lua-users.org/wiki/
Lua系の情報満載。各種サンプルコードやデバッガ・プロファイラ・x86マシン語変換など各種ツール・Eclipseプラグインとか本当にいろいろ。

  • 値。静的な型チェックなし。すべてファーストクラス(変数に代入・引数として受け渡し可能)。スクリプト言語にありがち。
    • nil
    • ブーリアン(true/false)
    • 数値(内部では(Luaのビルド設定によるが)double。DirectXのFPU設定変更に注意。整数も一括して浮動小数点数で扱うのでDirectXにFPUをfloat相当の精度に落とされると整数の精度まで24bitになってしまったりする。D3DCREATE_FPU_PRESERVEの使用を検討すること。)
    • 文字列
    • 関数
    • ユーザーデータ
    • スレッド
    • テーブル


テーブル
キー→値の表を表す。キーはnil以外何でも使えるし、異なる型が混合していてもよい。が、主に配列目的で数値(整数)のみをキーにするか、文字列のみをキーにしてオブジェクトとして扱うかが多い。

t1 = {["a"] = 1, ["b"] = 2}
t1 = {a = 1, b = 2}

t2 = {[1] = 1, [2] = 2}
t2 = {1, 2}

はそれぞれ同じ。配列のインデックスは1から始まることに注意。

t1["a"] = 5
t1.a = 5

t2[1] = 5

テーブルアクセスは[]で。ただしキーが文字列(ただし識別子と判定されること)の場合はt1.aのシンタックスシュガーあり。JavaScriptを思い出してまいりました。存在しないキーに代入すると追加され、存在しないキーを取り出すとnilが返るのはよくあるパターン。


関数
もちろんクロージャもあるよ!!あとコルーチンに対応。この2つにより、住み分けも面倒だしcppは1ファイルのみでゲーム全体をLuaで書いてしまえという結論に。最後にあまりにひどいようだったらプロファイラから箇所を特定してC++化するとか(というかそれもあまりやる気ないけど)。この方法論もゲームによって向き不向きがありそうだけど。

-- リテラル(といっていいのかは知らない)
function (a, b, c) ... end

-- それをfに代入する文
f = function (a, b, c) ... end

-- のシンタックスシュガー
function f(a, b, c) ... end

JavaScriptしか思い出さない。
と見せかけてまだまだ続く。

-- コロンを使った構文
function t:f(a, b, c) ... end

-- は、これのシンタックスシュガー
function t.f(self, a, b, c) ... end

-- さらに頭の中でここまで戻せないとだめ
t.f = function(self, a, b, c) ... end
t["f"] = function(self, a, b, c) ... end

-- 呼び出し
t:f(1, 2, 3)

-- は、これのシンタックスシュガー(selfにtを渡す)
t.f(t, 1, 2, 3)

私がみるとものすごいJavaScript臭がするが、ActionScriptを初めて見たとき、この言語をまともに使いこなしてる人が果たして何人いるのだろうかと考えたものだ。なんか今見たらAS2.0からクラスベースのJavaっぽいものになってた。いつの間にか静的型宣言までついてるし(なぜかPascal臭がするが、いやこれはむしろJavaFXScript…!?)いい時代になったものだ(…いやブーム去ってないか?)。
自分自身をselfと呼ぶ言語を知らない人はthisと読み替えてください。PythonRubyでselfならスクリプト言語としてselfなのかね。JavaScriptJavaからthisだが。まあこのように、Luaではselfは予約語などではないが処理系が勝手にその名前で追加する普通の引数名となっています(いやごめん他の言語は知らん)。obj.method(arg)のオブジェクト指向におけるメソッド呼び出しの本質がobj_method(obj, arg)であることを知っていればおっけー。…が、マジでわけわかんなくなった。


メタテーブル
Luaの全ての値にはメタテーブルが設定できる。値に演算子が適用されたときなどの挙動を設定することができる。演算子オーバーロードっぽいね!ただし、Luaからはテーブルのメタテーブルしか変更できない。でまあ、テーブルにキーアクセスがあった時、まずはそのテーブルから探して見つからなかった場合、メタテーブルの__indexに別のテーブルが設定されているときそこから探してくれるんですね。そのテーブルになかったらそれのメタテーブルの__indexから以下略。どうしてもなかったらnilを返す。なにこのプロトタイプチェーン。
で、これを使えばメソッドなり継承なり何でも出来そうなんですが、実際何でもできます。出来すぎてどうすればいいのか分かりません。
http://lua-users.org/wiki/ObjectOrientedProgramming
ここにオブジェクト指向の様々な実現例が載っていますが、意味不明(笑)。Javaオブジェクト指向機構を再現したライブラリを開発・配布してるところまであるし…。


ということで、手元にある「スクリプト言語による効率的ゲーム開発」に書いてあったものでとりあえず…。

-- MyClassクラス
-- 一度空のテーブルで初期化する
-- クラスもオブジェクト(テーブルだけど)とか言うと発狂する人はお帰り下さい
-- MyClassのstaticメンバはこの中に入れるといいんじゃないか?
MyClass = {}

-- MyClass.new = function new() ... と同意
function MyClass.new()
  -- インスタンス まずは空テーブル
  local obj = {}
  -- nameフィールドを作成・初期化
  obj.name = "yappy"
  -- objに無いものはMyClassから探すようにする
  setmetatable(obj, {__index = MyClass})
  -- インスタンスを生成・初期化して返した
  return obj
end

-- MyClass.say_name = function(self) ... と同意
function MyClass:say_name()
  print(self.name)
end

-- ["name"]に"yappy"が入っていて、
-- 無いものはMyClassから引くテーブルが返る
testobj = MyClass.new()
-- testobj.say_name(testobj) と同意
-- testobjには["name"]しかないのでMyClassに委譲→発見。
-- MyClass.say_name(testobj) 結局こうなる
testobj:say_name()

本ではMyClass:newで書いてたけどselfは不要というか、MyClassのコンストラクタってMyClassのインスタンスを生成するstaticメソッドみたいなもんだしなあ。でも.と:ってミスりやすそうだからクラス関係の.は全部:にするのもありなのかもしれない。
なんだか変な感じがするけど、その通りでnewでobjにnameだけでなく、say_name関数を追加すれば同じ事は出来ます。でも、この一見怪しい方式だとMyClassのメタテーブルの__indexに継承元のクラスを設定すると継承・オーバーライドができそう。newのチェーンで関数を書き換えればオーバーライドできるような気がしてスーパークラスの呼び出しチェーンが怪しい。…ちょっとこれ以上は想像力の限界が…。
まあとにかくとりあえずこれで行こう。


これ以外にはexplicit_globals.luaを導入してます。専用の関数を使ってグローバル変数を宣言するようにし、宣言していないグローバル変数を使おうとするとエラーにしてくれるモジュールです。タイプミスすると大抵存在しないグローバル変数へのアクセスとなるので面倒なバグを未然に防いでくれることが1回経験して分かりました。


結論:なんか一色で塗りつぶされた画面が出てくるだけのまんま。


なんか久しぶりに役に立つかもしれない記事書いた気がする。