ソロ Squirrel 勉強会まとめ
をやってきました。家で。まあつまり Squirrel について調べただけなのですが。
適当にまとめていきます。
資料とか
本家さん
日本語でよくまとまってるページ。
各種ページへのリンクもあるので嬉しい
- http://muffin.cias.osakafu-u.ac.jp/~matumoto/cgi-bin/xt.cgi?prog/squirrel
- http://muffin.cias.osakafu-u.ac.jp/~matumoto/cgi-bin/xt.cgi?prog/squirrel_anatomy
- http://muffin.cias.osakafu-u.ac.jp/~matumoto/cgi-bin/xt.cgi?prog/squirrel_lang
↑のページへまだ完全に移行していないみたいで、こっちにもまだいい情報がいっぱいあるっぽい。
Squirrel側で調べたこと
基本的な部分
// まずは出力
sq>print(1+2)
3
// 関数 sq>print(function(a,b){return a+b;}) (function : 0x00564600) // その場で作ってその場で呼び出し sq>print(function(a,b){return a+b;}(1, 2)) 3
// ループ sq>for(local i = 0; i < 5; i++) print(i) 01234
// 配列の定義 sq>local ar = [2, 3, 1, 4]; // ar が見つからないって怒られた sq>foreach (a in ar) print(a); AN ERROR HAS OCCURED [the index 'ar' does not exist] CALLSTACK *FUNCTION [main()] interactive console line [1] LOCALS [this] TABLE // スロットに入れろってことね sq>ar <- [2, 3, 1, 4] // 無事出力できた sq>foreach (a in ar) print(a) 2314
// 配列から指定された値を探す関数を定義 // 見つからなかったら null を返す(何も値を返さなければ null になる) sq>find <- function(ar, v) { foreach (i, a in ar) if (a == v) return i; } // findのテスト sq>print(find(ar, 1)); 2 sq>print(find(ar, 100)); (null : 0x00000000)
// find を削除 sq>delete this.find // 呼び出してみると、無事エラーになる sq>print(find(ar, 1)); AN ERROR HAS OCCURED [the index 'find' does not exist] CALLSTACK *FUNCTION [main()] interactive console line [1] LOCALS [this] TABLE
// 新しくスロットを用意して sq>ar2<-ar // 降順ソート sq>ar2.sort(function(a, b) { return a > b ? -1 : a < b ? 1 : 0; }); // 無事降順になってる sq>foreach (v in ar2) print(v); 4321 // 参照だから ar の方もソートされてしまうようだ sq>foreach (v in ar) print(v); 4321 // clone でコピーする sq>ar2<-clone ar; // 今度は昇順ソート sq>ar2.sort(); // 無事コピーされているようだ sq>foreach (v in ar2) print(v); 1234 sq>foreach (v in ar) print(v); 4321
// 配列を find できるデータを作る // : (array) でローカル変数を束縛できる。これを忘れて結構手間取った... sq>make_array <- function(array) { return { find = function (v) : (array) { foreach (i, a in array) if (a == v) return i; return null; } } } // 無事 find を呼び出せている sq>print(make_array([2, 1, 3, 4]).find(1)); 1
// クラスを作ってみる sq>class Hoge { x = 0; } // まあこれは当然 sq>print(Hoge().x); 0 // 直接アクセスできるのかよ! sq>print(this.Hoge.x); 0 // もう一度実験 sq>class Hoge { constructor() { x = 1; } x = 0; } // まあこれは当然 sq>print(Hoge().x); 1 // constructor が呼ばれていない sq>print(Hoge.x); 0
つまり class Hoge という定義は単なるマップでしかなくて、インスタンスを作るというのは、 new_hoge <- function() { local instance=clone Hoge; instance.constructor(); return instance; } とやっているのと同じってことかな。 まあスロットを追加したりできないっていう違いはあるけど。 継承関係がある場合は、 make_hoge <- function() { local instance=delegate clone Fuga : clone Hoge; instance.constructor(); return instance; } みたいにして親クラスへの移譲テーブルを持たせているだけだと思う。 こうすることで、Hoge にデータが無ければ親クラスの Fuga を見に行く事になる。 ん!?ということはですよ // こんなクラスがあるとき sq>Hoge <- class { x=0; ar=[1, 2, 3]; } // Hoge のインスタンスを作って ar[0] を書き換えると・・・ sq>Hoge().ar[0] = 2; // えええ!新しく作っても書き換わってる! sq>print(Hoge().ar[0]); 2 // ええええ!元となる Hoge が書き換わってるよ! sq>print(Hoge.ar[0]); 2 なんて分かりにくい動作だ・・・。 まあ各オブジェクトでちゃんと値を持たせたかったらコンストラクタの中で作れってことかな。
// [1] とは何ぞや sq>test<-{ x="fuga", [1]="hoge" } // 普通は . でアクセスできる sq>print(test.x) fuga // こっちは無理 sq>print(test.1) interactive console line = (1) column = (12) : error expected 'IDENTIFIER' // でアクセスする用ってことか sq>print(test[1]) hoge // こっちは無理。 では式を、. では id を渡せってことか sq>print(test[x]) AN ERROR HAS OCCURED [the index 'x' does not exist] CALLSTACK *FUNCTION [main()] interactive console line [1] LOCALS [this] TABLE
拡張メソッド的なあれ
を作る実験をしてみました
sq>ext <- function(e) { return { select = function(f) : (e) { foreach (v in e) yield f(v); } where = function(f) : (e) { foreach (v in e) if (f(v)) yield v; } } } // 偶数だけ取り出す sq>foreach (v in ext([1, 2, 3, 4, 5]).where(function(a) return a % 2 == 0)) print(v); 24 // 偶数だけ取り出して 2 倍する sq>foreach (v in ext(ext([1, 2, 3, 4, 5]).where(function(a) return a % 2 == 0)).select(function(a) return a * 2)) print(v); 48
なんかそれっぽい。
ちゃんと遅延実行されてるかどうか調べてみる。
ext <- function(e) { return { select = function(f) : (e) { foreach (v in e) { print("select: yield " + f(v) + "\n"); yield f(v); } } where = function(f) : (e) { foreach (v in e) if (f(v)) { print("where: yield " + v + "\n"); yield v; } } } } foreach (v in ext(ext([1, 2, 3, 4, 5]).where(function(a) return a % 2 == 0)).select(function(a) return a * 2)) print(v + "\n"); where: yield 2 select: yield 4 4 where: yield 4 select: yield 8 8
ちゃんと遅延実行になってる。素晴らしい。
ただ毎回 ext で囲むのって面倒だよね。何とかならないかなぁといろいろ試行錯誤した結果
ext_class <- class { constructor(gen) { this.gen = gen; } gen = null; current = null; select = function(f) { return ext_class(function() : (f) { foreach (v in gen) yield f(v); }()); } where = function(f) { return ext_class(function() : (f) { foreach (v in gen) if (f(v)) yield v; }()); } for_each = function(f) { foreach (v in gen) f(v); } _nexti = function(prev) { return (current = resume gen) == null ? null : 0; } _get = function(key) { return key == 0 ? current : this[key]; } } ext <- function(e) { return ext_class(function() : (e) { foreach (v in e) yield v; }()); } ext([1, 2, 3, 4, 5]).where(function(a) return a % 2 == 0).select(function(a) return a * 2).for_each(function(a) print(a + "\n"))
こうなった。素晴らしい。
ただこれって毎回 where とか select のオブジェクトが作られててもったいないよね。とか思ったので更に試行錯誤。
ext_class <- class { constructor(gen) { this.gen = gen; } gen = null; current = null; static static_select = function(gen, f) { return ext_class(function() : (gen, f) { foreach (v in gen) yield f(v); }()); } select = function(f) return static_select(gen, f); static static_where = function(gen, f) { return ext_class(function() : (gen, f) { foreach (v in gen) if (f(v)) yield v; }()); } where = function(f) return static_where(gen, f); static static_for_each = function(gen, f) { foreach (v in gen) f(v); } for_each = function(f) return static_for_each(gen, f); _nexti = function(prev) { return (current = resume gen) == null ? null : 0; } _get = function(key) { return key == 0 ? current : this[key]; } }
まずはこんな感じに。
毎回オブジェクトが作られてるのは同じだけど、static な関数に渡してるので、本体を毎回書くよりは小さいはず。
(いやそもそも毎回オブジェクトを作るたびに関数の本体もコピーしてるのかな?もししてないなら static にするのは無意味)
ただまあこれだと、作るオブジェクトの数が増えてくるとやっぱり困るよねとか思ったり。
でもまあこの辺りで限界かなぁと思ってたんですけど、どうやら _get は hoge.bar とやったときに「bar は無いの?」と聞いてくれるみたいなので、これを使って更にオブジェクトを減らしましょう。
あと _nexti で返すのはインデックスじゃなくてもいいらしいので、そのまま値を返せば current も消し去ることができそうです。
ext_class <- class { constructor(gen) { this.gen = gen; } gen = null; static static_select = function(gen, f) { return ext_class(function() : (gen, f) { foreach (v in gen) yield f(v); }()); } static static_where = function(gen, f) { return ext_class(function() : (gen, f) { foreach (v in gen) if (f(v)) yield v; }()); } static static_for_each = function(gen, f) { foreach (v in gen) f(v); } static static_get = function(key) { switch (key) { case "nexti_gen": return key.value; case "select": return function(f) return static_select(gen, f); case "where": return function(f) return static_where(gen, f); case "for_each": return function(f) return static_for_each(gen, f); default: return this[key]; } } static static_nexti = function(gen) { return { _tostring = function() { return "nexti_gen" } value = resume gen } } _nexti = function(prev) { return static_nexti(gen) } _get = function(key) { return static_get(key); } } ext <- function(e) { return ext_class(function() : (e) { foreach (v in e) yield v; }()); } ext([1, 2, 3, 4, 5]).where(function(a) return a % 2 == 0).select(function(a) return a * 2).for_each(function(a) print(a + "\n"))
これで毎回 gen と _nexti と _get の分しか使わなくなっていい感じに。
毎回文字列を比較する分だけ速度は遅くなりそうですけど、まあ許容範囲内ってことにしておきます。
自分で言うのもなんですけど、めちゃめちゃいい感じにできたと思います。
C++で調べたこと
スタックの操作めんどそう
↓
C++ で適当にラップして使えるようにしてみるかな
↓
いやでもこれって誰かやってるんじゃね?
↓
多くの開発者は単純なテンプレートバインディングを好む。 LuaにとってLuaPlusは優秀であるが、クラス/インスタンスを支援するための簡単なソリューションを提供しない。 luabindは非常に強力であり、クラス/インスタンスを支援にも優れているが、設定やコンパイルもより複雑になっており、コンパイル時間も長くなり、デバッグビルドでは長大なシンボルテーブルが生じてしまう。そのような複雑なサポートが必要なアプリケーションもあるが、ゲームには常に必要であったり望まれていたりはしない。 Squaddは、次のようなバインディングがある。
http://muffin.cias.osakafu-u.ac.jp/~matumoto/cgi-bin/xt.cgi?prog/squirrel
- DxSquirrel (単純なマクロによるバインディングシステム)、
- Squadd (boostベースのluabind風の機能)、
- SqPlus (商用ゲーム開発に必要なすべてを提供し、単純で簡潔なクラス/関数/変数/定数バインディング、C/C++からのスクリプト関数呼び出し、非常に軽量なテンプレートコードを含む)
余裕でありました。
あと sqdbg っていうデバッガもあるみたいで、Eclipse からも使えるらしい。まだ試してないから何とも(というか Eclipse 入れるのめんd)
明日も勉強会する予定なので追記すると思います
言語拡張とか
拡張メソッド的な、関数式を大量に作るようなのを書いてると、
function(a) return a % 2 == 0
とか書くのが面倒になってくる。
これってもっと簡単に、例えば C# だったら
a => a % 2 == 0
とか書けるよねっていう。
ということでちょっと Squirrel 本体に手を入れる実験をしてみた。
実を言うと、Squirrel 本体のコードを触るというのが目的で、短く書けるようにしたいよねっていうのは無理矢理なこじつけなので「これって本体に手を入れたくなるぐらい欲しい機能なの?」とかって疑問は考えないように。
まずは文法を考える。
C# なんかだと => が見つからないとそれ以前にある式がラムダ式であるって判断が付かない気がするので、ちょっと文法的にめんどそうだよねってことで諦め。
でまあこんな感じに。
// 1引数を取る関数 \a -> a % 2 == 0 // 2引数を取る関数 \a b -> a + b // 1引数を取って1引数の束縛を行う \a (b) -> a + b // 2引数を取って2引数の束縛を行う \a b (c, d) -> a + b + c + d // 引数も束縛も無し \ -> 10 // 引数無しで束縛2つ \(a, b) -> a + b
function 式だと可変長引数とかデフォルト値とか使えるんだけど、この新しい構文では不可ということにしました。
あと当然中身は単一の式で、値を返す必要があります。 -> が return だと思って下さい。
実際に手を入れてみる。Squirrel 2.2.4 に手を入れてます。
- > をトークンとして認識する必要があるので、字句解析に少し手を入れる。
まずトークンの種類を追加して・・・
--- sqcompiler.h.org 2007-10-15 05:13:18.000000000 +0900 +++ sqcompiler.h 2010-08-01 06:18:11.733000000 +0900 @@ -70,6 +70,7 @@ #define TK_STATIC 322 #define TK_ENUM 323 #define TK_CONST 324 +#define TK_LAMBDA_ARROW 325 typedef void(*CompilerErrorFunc)(void *ud, const SQChar *s);
トークンの解析で認識してやるようにする
--- sqlexer.cpp.org 2008-02-17 18:16:16.000000000 +0900 +++ sqlexer.cpp 2010-08-01 06:18:11.717375000 +0900 @@ -233,6 +233,7 @@ NEXT(); if (CUR_CHAR == _SC('=')){ NEXT(); RETURN_TOKEN(TK_MINUSEQ);} else if (CUR_CHAR == _SC('-')){ NEXT(); RETURN_TOKEN(TK_MINUSMINUS);} + else if (CUR_CHAR == _SC('>')) { NEXT(); RETURN_TOKEN(TK_LAMBDA_ARROW); } else RETURN_TOKEN('-'); case _SC('+'): NEXT();
あとは実際に構文解析してる所に手を入れてオペコードを生成すると。
--- sqcompiler.cpp.org 2008-12-21 01:27:36.000000000 +0900 +++ sqcompiler.cpp 2010-08-01 06:18:11.733000000 +0900 @@ -208,10 +208,11 @@ case TK_FOREACH: ForEachStatement(); break; case TK_SWITCH: SwitchStatement(); break; case TK_LOCAL: LocalDeclStatement(); break; + case TK_LAMBDA_ARROW: case TK_RETURN: case TK_YIELD: { SQOpcode op; - if(_token == TK_RETURN) { + if(_token == TK_RETURN || _token == TK_LAMBDA_ARROW) { op = _OP_RETURN; } @@ -730,6 +731,7 @@ } break; case TK_FUNCTION: FunctionExp(_token);break; + case _SC('\\'): LambdaExp(_token); break; case TK_CLASS: Lex(); ClassExp();break; case _SC('-'): UnaryOP(_OP_NEG); break; case _SC('!'): UnaryOP(_OP_NOT); break; @@ -1189,6 +1191,51 @@ CreateFunction(_null_); _fs->AddInstruction(_OP_CLOSURE, _fs->PushTarget(), _fs->_functions.size() - 1, ftype == TK_FUNCTION?0:1); } + void LambdaExp(SQInteger ftype) + { + Lex(); + + SQFuncState *funcstate = _fs->PushChildState(_ss(_vm)); + funcstate->_name = _null_; + SQObject paramname; + funcstate->AddParameter(_fs->CreateString(_SC("this"))); + funcstate->_sourcename = _sourcename; + while(_token!=_SC('(') && _token!=TK_LAMBDA_ARROW) { + paramname = Expect(TK_IDENTIFIER); + funcstate->AddParameter(paramname); + } + //outer values + if(_token == _SC('(')) { + Lex(); + while(_token != _SC(')')) { + paramname = Expect(TK_IDENTIFIER); + //outers are treated as implicit local variables + funcstate->AddOuterValue(paramname); + if(_token == _SC(',')) Lex(); + else if(_token != _SC(')')) Error(_SC("expected ')' or ','")); + } + Lex(); + } + + if (_token != TK_LAMBDA_ARROW) Error(_SC("expected '->'")); + + SQFuncState *currchunk = _fs; + _fs = funcstate; + Statement(); + funcstate->AddLineInfos(_lex._prevtoken == _SC('\n')?_lex._lasttokenline:_lex._currentline, _lineinfo, true); + funcstate->AddInstruction(_OP_RETURN, -1); + funcstate->SetStackSize(0); + //_fs->->_stacksize = _fs->_stacksize; + SQFunctionProto *func = funcstate->BuildProto(); +#ifdef _DEBUG_DUMP + funcstate->Dump(func); +#endif + _fs = currchunk; + _fs->_functions.push_back(func); + _fs->PopChildState(); + + _fs->AddInstruction(_OP_CLOSURE, _fs->PushTarget(), _fs->_functions.size() - 1, ftype == _SC('\\')?0:1); + } void ClassExp() { SQInteger base = -1;
最初の部分は -> を return と解釈するための修正で、次のは \ が来たら新しい関数式の構文として扱う部分で、最後のがその構文を解釈するところ。
function 式の syntactic sugar でしかないので、結構簡単に作れたり。
これで小さな function 式を書くのが楽に。
例えば昨日の例の拡張メソッド的なのを使うのは、
ext([1, 2, 3, 4, 5]).where(function(a) return a % 2 == 0).select(function(a) return a * 2).for_each(function(a) print(a + "\n"))
こう書いていたのが
ext([1, 2, 3, 4, 5]).where(\a -> a % 2 == 0).select(\a -> a * 2).for_each(\a -> print(a + "\n"))
こんな風に。美しいですね。
ちなみに ext_class 側を出来る限り新しい関数式に書き直してみるとこうなる。
ext_class <- class { constructor(gen) { this.gen = gen; } gen = null; static static_select = \gen f -> ext_class(function() : (gen, f) { foreach (v in gen) yield f(v) }()); static static_where = \gen f -> ext_class(function() : (gen, f) { foreach (v in gen) if (f(v)) yield v }()); static static_for_each = function(gen, f) foreach (v in gen) f(v) static static_get = function(key) { switch (key) { case "nexti_gen": return key.value; case "select": return \f -> static_select(gen, f); case "where": return \f -> static_where(gen, f); case "for_each": return \f -> static_for_each(gen, f); default: return this[key]; } } static static_nexti = \gen -> { _tostring = function() { return "nexti_gen" } value = resume gen } _nexti = \prev -> static_nexti(gen) _get = \key -> static_get(key); } ext <- \e -> ext_class(function() : (e) { foreach (v in e) yield v }())
これはやりすぎ。特に static_nexti の定義は、ブロックで囲んでるせいで新しいマップを生成して返しているというのがすごく分かりにくい。