*[hatefu:labs.yaneu.com/20090905/] コンピュータ将棋プログラムをLISPで書く
「コンピュータ将棋プログラムをLISPで書く」と言うとコンピュータ将棋開発関係者にすら完全にネタかと思われているのが実状ではあるが、私はこれを機にその誤解を解いておきたい。
ここでは、私がC#で書いたLISPエンジンのソースを公開し、これが実際にコンピュータ将棋プログラムの開発において非常に有効であることを示す。
* YaneLisp version 1.10
今回の記事はあまりに長文なので最後まで読む前に眠くなる人のために、まず始めに私が実装したLISPのバイナリとソースを配布しておく。ライセンスはNYSLとする。
勢いに任せて実装したので、かなり雑な作りだが、必要ならばC#側で関数を追加するなりすればいいと思う。このLISPの製作に要した時間は丸2日ぐらい。
# YaneLisp
YaneLispのバイナリとソース(2010年1月2日バージョン)
http://labs.yaneu.com/20090905/yanelisp110.zip
** 今後のバージョンアップは、このページでこっそり行なう。
* 何故LISPはネタだと思われるのか?
普通、コンピュータ将棋は、評価関数が同等であればあとは、1秒間にいかにたくさんの局面を調べられるかが肝要である。そのために上位のコンピュータ将棋ソフトは血の滲むような高速化のための努力をしているのである。
最強クラスのコンピュータ将棋ソフトであるBonanzaとGPS将棋に関してはソースが公開されている。そのどちらも高速化には数々のテクニックが使われている。
それなのに、LISPなんかに本当にトッププログラムと互角にやり合えるだけのポテンシャルがあるのか?あなたがそう考えるのは当然のことである。あせらずゆっくり話を聞いて欲しい。
# Bonanza
Bonanza - The Computer Shogi Program
http://www.geocities.jp/bonanza_shogi/
# GPS将棋
GPSshogi - PukiWiki
http://gps.tanaka.ecc.u-tokyo.ac.jp/gpsshogi/
* Bonanza vs GPS将棋
ところで、BonanzaとGPS将棋とは、開発方針において全く対極にある。
Bonanzaは、Cで書かれている。C++ではない。Cなのだ。#defineによるマクロも多用されている。例えば、局面を指し手に従って1手進める関数は、先手用の関数と後手用の関数と2種類の関数がある。その2つはほぼ同じコードである。コピペして文字置換してあるのだが、ソースを1箇所修正するとそれに付随して何カ所も修正しなくてはならない。ソースの見通しは良いが、ソースのメンテナンスは大変である。
一方、GPS将棋は、C++のtemplateを使いたくってある。『Modern C++ Design』に出てくるテクニックの数々は言うに及ばず、boostも駆使してあり、かつ、GCCにかなり依存する形で書かれている。私は、今年の正月をまるまる使ってある程度解読したが、これは常人には解読不可能と言ってもいいだろう。
GPS将棋は、Visual C++でビルドすることすらままならず、templateを駆使してあるのでコンパイルに膨大な時間がかかり(Bonanzaは数秒でビルドが終わる)、ターンアラウンドタイムが長いことこの上ない。ソースはこの上なく見通しが悪く、このソースを理解できる能力があるなら、GPS将棋のソースを流用せずとも自分でコンピュータ将棋プログラムが書けそうである。
以上のような理由により、コンピュータ将棋開発者のなかにBonanzaのソースを参考にしたりする者は多いが、GPS将棋のソースを参考にしようという者はほとんどいない。参考にしたくても、そう簡単に参考にできるシロモノではないからである。
# GPS将棋のソースレビュー
コンピュータ将棋 - PukiWiki GPS将棋のソースレビュー
http://shogi.yaneu.com/index.php?GPS%BE%AD%B4%FD%A4%CE%A5%BD%A1%BC%A5%B9%A5%EC%A5%D3%A5%E5%A1%BC
Bonanzaのソースをベースにしていたり、Bonanzaで使われている学習メソッドを用いてあるものは「ボナンザチルドレン」略して、「ボナチル」と呼ばれる。「ボナチル」は居ても「GPSチルドレン」はほとんど居ない。私はGPSチルドレンに最も近い存在は棚瀬将棋(商品名:東大将棋無双)ではないかと思う。
* C++ templateを駆使して書くべきか?
C++ templateを駆使して書けば、Bonanzaのように似た処理を何ヶ所にも書く必要がなくなる。しかし、コンパイル時間が伸びるし、書くのにかなりのテクニックを要する。ソース修正ごとに結構頭を悩ませなければならない。これでは開発効率が本当に上がっているとは言い難い。
かと言って、Bonanzaのように同じソースを何ヶ所にも書くとケアレスミスやコピペ忘れ、修正忘れなどが発生する。Bonanzaで使われているbitboardのテクニックは、C++ templateを使わないBonanzaにとって、そのコーディングをしやすくするために必要不可欠だったのではないかと私は思う。
実際、Bonanzaのbitboardではbitboardを使わない場合に比べて高速化が図れているとは言い難い。これについての詳しい話は割愛する。
* コンピュータ将棋ソフトはこう書かれるべき
ここまでの内容をまとめるとこうなる。
** C++ templateを駆使したプログラムは熟練したプログラマにとっても難解でトータルでの開発効率がかえって落ちる。特にコンピュータ将棋のようにコードを頻繁に修正しないといけないプログラムには向かない。
** 似たコードをコピペで書くのは避けるべき。どうしても先手用のコードと後手用のコードとを事前に用意して高速化を図りたくなるが、そういう場合は、片方のコードだけ書いて、もう片方のコードは正規表現置換などで自動的に生成すべき。
* ところが正規表現置換だけでは解決しない
例えば、Bonanzaの評価関数である evaluate.c には次のようなコードがある。
>>
n2 = 0;
bb = BB_BLANCE;
while ( BBToU(bb) ) {
sq = FirstOne( bb );
Xor( sq, bb );
list0[nlist] = f_lance + sq;
list2[n2] = e_lance + Inv(sq);
score += kkp[sq_bk0][sq_wk0][ kkp_lance + sq ];
nlist += 1;
n2 += 1;
}
bb = BB_WLANCE;
while ( BBToU(bb) ) {
sq = FirstOne( bb );
Xor( sq, bb );
list0[nlist] = e_lance + sq;
list2[n2] = f_lance + Inv(sq);
score -= kkp[sq_bk1][sq_wk1][ kkp_lance + Inv(sq) ];
nlist += 1;
n2 += 1;
}
for ( i = 0; i < n2; i++ ) { list1[nlist-i-1] = list2[i]; }
<<
BB_BLANCEの「BB_」はbitboardの意味で、BLANCEのBはBlack(先手)、WならWhite(後手)を意味する。(black/whiteという呼び名はチェスに由来する)
ここに出てくる「LANCE」は将棋の駒の「香」用のコードであることを意味して、この他に「歩」「香」「桂」「銀」「金」「角」「飛」「と」「成香」「成桂」「成銀」「馬」「龍」の13種類の駒×先手後手 = 26個の類似コードが evaluate.c に存在する。
もしかしたら「各駒種類ごとのbitboardをすべて一つの配列に入れて、配列の添え字でアクセスすれば26種類も類似コードは不要なのじゃないか?」と指摘する人もいるかも知れないが、そんな実行時のオーバーヘッドは到底許容できないから、こうして各駒ごとに処理を展開しているわけである。
また、先手用のコードを後手用にするのは確かに正規表現置換で自動的に生成できるが、これら13種類(王を除く駒で成り駒は別の種類の駒としてカウントする)の駒種ごとのコードをベースとなるコードから生成するには単純な正規表現置換では不十分である。繰り返し的なことが出来なくてはならない。
* スクリプトで書くべきか?
外部スクリプトで13×2個のコードを生成しても良いが、見通しが悪くなるので元のソースと別のところにスクリプトを書きたくない。
それと同時に、元のソースの入力のときにIntelliSenseが利くようにしたいので、スクリプトコードは元のソース入力を阻害して欲しくない。
すなわち、コードを生成するためのスクリプト的なコードは、ソース中にコメント行として書くようになっていなければならない。これならば、IntelliSenseが利く状態でスクリプトを書くことが出来る。
>>
//%
//% ここにスクリプトのためのコードを書く
//% これならば、IntelliSenseを阻害しない。
//%
n2 = 0;
bb = BB_BLANCE;
while ( BBToU(bb) ) {
sq = FirstOne( bb );
Xor( sq, bb );
<<
* コードの生成のためにどのようなスクリプトが必要か?
だいたい何がやりたいか見えてきたと思う。
では、このようなコードの生成のために求められているのはどのようなスクリプトか?
** 文字列が扱いやすいこと
** 拡張が容易であること
** 実行エンジンが書きやすいこと
** 生成するテキストはわずかなので実行速度は遅くとも良い
どう見てもLISPです。本当にありが(ry
そんなわけで、LISPがあればちょうどいいということがわかった。
* LISPエンジンは自前実装すべき
LISPの実行エンジン自体は実装が容易である。出来れば、今回の仕様向きにカスタマイズされたものが良いので自前で実装してみることにした。
私の書いたLISPの実行エンジン本体は次のような1画面におさまる程度のプログラムである。C#で書くと、GC(garbage collection)があって、昔(15年ぐらい前に)、Cで書いたときよりずいぶん簡単に書けるなぁというのが率直な感想である。あまりに楽に書けるのでとりあえず動くバージョンは数時間で書けてしまった。
その後、自分が欲しい機能をいろいろ追加して行ったのだが、機能がみるみるうちに増えていくのは見ていて楽しい。
言語としては物足りないかも知れないが、限られたソース生成に使うのでまあこれくらいでもいいだろう。必要ならば、都度C#側で追加していけばいいのだし。
>>
// SExpというのはS式のことである。
public SExp eval(SExp s)
{
if (s == null || s.elms == null)
return null; // 評価不可能
var exp = s.elms as SExp;
if (exp!=null)
{
// ( (add 1 2) (sub 4 3) )のようにコマンドが並んでいるケース。
// この場合、後続するリストもすべて実行する必要がある..
if (s.next!=null)
{
var o1 = eval(exp);
var o2 = eval(s.next); // 最後の要素が評価されてこのevalの値になるはずだが
return o2 ?? o1;
}
return eval(exp);
}
var command = s.elms as VarName;
if (command!=null)
{
// 命令なので実行する。
return doCommand(command.name, s);
}
// stringなら無視しとけばいいか。
// return string2SExp( command + "は、evalできない。場所 : " + s.line + " 行 (" + s.sourcePos + ")");
if (!isNextNull(s))
return eval(s.next);
return null; // 駄目ぽ
}
<<
* YaneLispの特徴
** C/C++/C#のソース部分をLISP上では文字列として扱えるようなモードがある。これをimportモードと呼ぶ。(includeモードは、そうではなく単にファイルを LISPの文として読み込む。)
例えば、次のようにすれば、XXXとYYYの部分が、{ x_A , x_B , x_C } × { y_A , y_B , y_C }の9種類に置換され、展開される。
>>
//% (write
inline int x_A(x,y) { return x + y; }
inline int x_B(x,y) { return x + y*2; }
inline int x_C(x,y) { return x*2 + y; }
inline int y_A(x,y) { return x + y; }
inline int y_B(x,y) { return x + y*4; }
inline int y_C(x,y) { return x*4 + y; }
//% )
//% (let list 'x_A' 'x_B' 'x_C')
//% (let list2 'y_A' 'y_B' 'y_C')
//% (foreach e2 list2
//% (foreach e list ( write
//% (replace (replace
void function_XXX_YYY()
{
for(int y = 0; y<10; ++y)
for(int x = 0; x<10; ++x)
{
FUNC_XXX(x,y) * FUNC_YYY(x,y);
}
}
//% 'XXX' e)
//% 'YYY' e2)
//% )))
<<
evaluate.c の例で言えば、{先手 , 後手 } × { 駒種(13種類) } = 26通りのコードを上のコードみたくして生成できるということである。
その他のYaneLispの特徴として…
** 日本語の変数名が使える。
** 括弧として (){}《》【】〔〕〈〉[]が使える。同じ種類の括弧が対応している必要がある。同じ種類の括弧で閉じていなければエラーになる。普通のLISPより括弧の種類が豊富なので対応する括弧を見失いにくい。
** '…' "…" 「…」『…』 などで文字列を使える。 "エラー" + error; のような文字列を書きたいときも、'' で囲っても文字列が表現できるので '"エラー" + error; ' あるいは 「 "エラー" + error; 」 と書けば良い。C/C++のように "" をescapeしなくて済む。
** foreach,while,if,for,forever,downfor,loop,switchなどで制御構造を記述できる。
** 他のファイルを動的にimport/includeすることが出来る
** 関数を定義できる。
** すべてはfirst class objectなので、式を変数に突っ込んでおいてあとでevalすることも出来る。
** 関数のなかでlocal変数を使える。
* YaneLispの詳しい仕様
YaneLispのUnitTestをこのページの末尾におまけとして貼り付けておく。これがYaneLispの仕様のすべてである。最初にも書いた通り、2日程度で作ったものなのでかなり雑な作りだが必要ならばLISPのソース(C#で書かれている)をいじって必要な関数を追加していけば良いと思う。
* まとめ
以上のように、C/C++/C#のコメント中にLISPのコードを埋め込み、それによりソース生成を行なうことにより、ソースの可読性をあまり損なわず、かつ同じコードを繰り返し記述することを避けることが出来る。
これはコンピュータ将棋ソフトの開発のために私(やねうらお)が必要に迫られて編み出した手法だが(コンピュータ将棋に限らなければおそらく前例があるだろうけど)、従来、C++ templateを駆使して書いていたソフトウェアにもこのような開発手法を適用できるケースが結構あると思う。
是非、これを機にLISPを自作してみていただきたい。(YaneLispを有効活用してもらっても良いが、LISP自体の実装は容易なので是非自分好みのものを作っていただきたい。)
* 追記(2009.09.05)
はてブで次のようなコメントを頂戴している。
>>
athos 単なるコード生成に使うんなら、別に Lisp である必要もないし、自作する必要もないでしょ。
<<
これに関しては、私の説明が不十分だったかも知れない。
** 私がLispを選んだ理由
parserを書くのが楽だから。今回実装したLispと同等の機能を持つ他の言語をたかだか数時間で設計/実装するのは実質的に不可能だと思う。(私は、YaneLispの基本部分は、数時間で書き上げている。)
** Lispを自作した理由
私の使い勝手の問題。典型的なLispの実装は、括弧がいっぱいでとても読みにくくなる。私が作ったYaneLispのほうは、いろんな種類の括弧が使える。同じ種類の括弧が閉じていなければエラーになる。あと、文字列をダブルクォートだけではなく、シングルクォートや「…」『…』などでも表現できる。
あと、Unicode対応。文字列や変数名として日本語が使えて欲しい。また、DSLっぽいことも将来的にはさせたいので、そのコードをC#で記述したい。自分で作らないとそういう拡張が容易ではない。
** YaneLispが優れた実装だと言いたいのではない
誤解しないで欲しいのは、既存のLispの実装(Common LispやScheme、etc..)より私の実装のほうが優れていると言いたいわけではないということだ。結局、私の手に馴染むかどうかの問題である。
既存の実装についてその仕様を覚える時間、使い勝手を考えれば、いかにその処理系が、実行速度が速く、バグがほとんど無く、動作が安定していようと私は自分で作ったほうが短い時間で最終的な成果物(コンピュータ将棋ソフト)を書けると言うことである。
以上の理由から、結局、今回のケースでLispを選択するのは自然で、かつLispの自作も必然だと私は考えているわけである。
* 関連記事
# コンピュータ将棋 Puki-Wiki
コンピュータ将棋wiki
http://shogi.yaneu.com/
# 関連書籍
OnLisp
http://www.amazon.co.jp/exec/obidos/ASIN/4274066371/aaaaab0c-22/ref=nosim/
計算機プログラムの構造と解釈
http://www.amazon.co.jp/exec/obidos/ASIN/489471163X/aaaaab0c-22/ref=nosim/
* おまけ -- YaneLispのUnitTest
以下に貼り付けてあるのは、YaneLispのUnitTestであり、かつこれが仕様のすべてである。
>>
// 文字列とは
// quoteされたもの(ダブルコーテイションで囲われたもの or シングルクォートで囲まれたもの、
// あと、「」『』で囲まれたもの。ソースはutf-16にて記述する。)
// 数値、記号(+,-,*,/)で始まるもの
// それ以外は変数名・関数名。変数名と関数名との区別は無い。
// 数値の加算
// 返し値は文字列として扱われる。また加算のときに文字列は強制的に数値に変換される。
assertEval("(add 1 2 3 4 5 6 7 8 9 10)", "55");
assertEval("(add '1' '2' '3' '4' '5' '6' '7' '8' '9' '10')", "55");
// 小数も使える。
assertEval("(add 1.5 2.7)", "4.2");
// 負数も使える。
assertEval("(add 3 -5)", "-2");
// 数値の減算。10-2-3 = 5
assertEval("(sub 10 2 3)", "5");
// 数値の掛け算。2*3*4 = 24
assertEval("(mul 2 3 4)", "24");
// 数値の割り算。10/2/2 = 2.5
assertEval("(div 10 2 2)", "2.5");
// 内部的には、数値は演算するまでは文字列として扱われる。
// また演算はすべてC#のdouble型(倍精度浮動小数)で行なわれる。
// 文字列の連結。
// addしたものは文字列である。
// getは文字列を連結するので次のような結果になる。
assertEval("(get 1 2 3 4 5 (add 6 7) 8 9 10)", "12345138910");
// '' "" 「」 『』で囲まれたものは文字列
assertEval("(get 「日本語」'も'『使えるよ』)", "日本語も使えるよ");
// 日本語の変数名も使える
assertEval("(set 歩の価値 250)(get 歩の価値)", "250");
// 未定義の変数を表示させようとした場合、#undefが返る
assertEval("(get x)", "#undef");
// printは変数も保持している内容を再度conv2SExpで変換して、eval可能な文字列にする。
// S式のシリアライズみたいなもの。
// 定義されていない変数に対してはundefになる。
// xを評価しようとする→#undefが返る → printはそれを忠実に表示するために、
// #undefは文字列なので文字列化するためにコーテイションで囲って返す
assertEval("(print x)", "'#undef'");
// 書式 ( let 変数名 代入する文字列 )
// letは、変数名を指定して、文字列 or S式の内容を格納する。
// ' ' か " "で囲まれていればそれは文字列。
// 代入する側にある変数名は一切評価されない。
// 本家LISPの '(...) という表記に対応する。
assertEval("(let x 'AAA')(print x)", "'AAA'");
assertEval("(let x 'AAA' 'BBB')(print x)", "'AAA' 'BBB'");
// letは中身を評価せずにそのまま格納して、printも中身を評価せずにそのまま表示。
// よって、次のようになる。
assertEval("(let x 'AAA' (cat 'BBB' 'CCC'))(print x)", "'AAA' (cat 'BBB' 'CCC')");
assertEval("(let x 'AAA') (let y 'BBB' 'CCC') (print x y)", "'AAA' 'BBB' 'CCC'");
// (get 変数名1 変数名2 …)
// getは変数名1,2,…を評価して、それを式の値にする。
// 文字列は、'…'として格納している。
// getは評価したときに要素と要素との間にスペースは入らない。
assertEval("(let x 'AAA' 'BBB')(get x)", "AAABBB");
assertEval("(let x 'AAA')(let y 'BBB')(get x y)", "AAABBB");
assertEval("(let x 'AAA' 'BBB' 'CCC')(get x)", "AAABBBCCC");
// printとgetとの違い。
// letは中身を評価せずにそのまま持っている。printはだからそのまま表示する。
// これをgetで表示しようとしたとき、getは変数の中身をそれぞれ評価しながら表示する。
// よって動作は以下のような違いが生じる。
assertEval("(let x 'AAA' 'BBB' (get 'CCC' 'DDD'))(print x)", "'AAA' 'BBB' (get 'CCC' 'DDD')");
assertEval("(let x 'AAA' 'BBB' (get 'CCC' 'DDD'))(get x)", "AAABBBCCCDDD");
assertEval("(let y 'EEE')(let x 'AAA' 'BBB' (get 'CCC' 'DDD' y))(get x)", "AAABBBCCCDDDEEE");
assertEval("(let y 'EEE')(let x 'AAA' 'BBB' (get 'CCC' 'DDD') y)(get x)", "AAABBBCCCDDDEEE");
// getは出現した変数はすべて再帰的に評価される。(循環参照に注意!)
assertEval("(let x 'AAA')(let y x)(get y)", "AAA");
// setはletと違い、代入のときに変数名はすべて評価される。
assertEval("(let x 'AAA')(set y 'BBB')(let z x y)(print z)", "x y");
assertEval("(let x 'AAA')(set y 'BBB')(let z x y)(get z)", "AAABBB");
assertEval("(set x 'AAA' 'BBB')(get x)", "AAABBB");
assertEval("(let z 'DDD')(set x 'AAA' 'BBB' 'CCC' z)(let y x)(get y)", "AAABBBCCCDDD");
assertEval("(let z 'DDD')(set x 'AAA' (get 'BBB' 'CCC'))(set y x)(get y)", "AAABBBCCC");
// addtoは文字列を変数に追加
assertEval("(addto x 'AAA')(addto x 'BBB')(get x)", "AAABBB");
// 書式 ( foreach 変数名 コレクション名 (実行する式) )
// foreachはコレクションをひとつずつ変数に代入しながら、
// 後続する命令を実行する。
// 複数実行するなら、さらに括弧でくくること。
// 例 : foreach x xs ( (command1) (command2) … )
// また、foreachの値は、評価した式を連結したものになる。
assertEval("(set xs 'AAA' 'BBB' 'CCC' ) (foreach x xs (get x))","AAABBBCCC");
// replaceは文字置換した値を返す
assertEval("(set xs 'AAA' 'BBB' 'CCC' ) (foreach x xs (get (replace '123xxx456xxx' 'xxx' x) ' '))",
"123AAA456AAA 123BBB456BBB 123CCC456CCC ");
// ファイル or 標準出力にoutする。
// assertEval("(out 'AAA' 'BBB' 'CCC' )"),"AAABBBCCC");
// write という、ファイルに出力する命令を用意する。
// writeは'outfile'という変数に格納されているファイル名のファイルに出力する。
// (set outfile 'test.log') (write 'ABCDEF') とやれば、
// test.logに'ABCDEF'が出力される。
// 2重のforeach
assertEval("(let list 'x_A' 'x_B' 'x_C')(let list2 'y_A' 'y_B' 'y_C')(foreach e list(foreach e2 list2 (replace (replace 'XXXYYY' 'XXX' e2) 'YYY' e) ))", "y_Ax_Ay_Bx_Ay_Cx_Ay_Ax_By_Bx_By_Cx_By_Ax_Cy_Bx_Cy_Cx_C");
// 未定義の変数をsetした場合、それをsetした瞬間に評価され、結果は#undefになる。
assertEval("(set list x_A x_B x_C)", "#undef#undef#undef");
// これは無限再帰で、再帰が深いので、エラーになる。
// assertEval("(let x y)(let y x)(get y)", "x");
// これは、set y x のときに、xの定義を参照しに行き、そこでyが使われているが、
// yはまだsetが完了していないので未定義であり、結局、yにはこの未定義であるy(#undef)が
// 代入される。
assertEval("(let x y)(set y x)", "#undef");
// loopは繰り返す…が、最後に評価されたものが式の値になるので結果は最後に評価された式になる。
assertEval("(loop 3 (get 'ABC')", "ABC");
assertEval("(loop 3 (get 'ABC')(get 'DEF')", "DEF");
// 回数は3×5回で15になっているので、きちんとループで実行されていることがわかる。
assertEval("(set sum 0)(loop 5 (set sum (add sum 3))", "15");
// loopの回数を指定するところには、変数も指定できる。変数の値は変化しない。
assertEval("(set total 7)(set sum 0)(loop total (set sum (add sum 3))", "21");
// tolower/toupperは小文字化する
assertEval("(set x 'Abc')(set y 'deF')(tolower z x y)(get z)", "abcdef");
assertEval("(set x 'Abc')(set y 'deF')(toupper z x y)(get z)", "ABCDEF");
// arrayによって配列とみなして任意の要素を取り出せる
assertEval("(set x 'AAA' 'BBB' 'CCC')(set y (array x 2))(get y)", "CCC");
// arrayによって、配列の配列からも任意の要素を取り出せる。
assertEval("(let x ('AAA' 'BBB')('CCC' 'DDD') )(set y (array (array x 1) 1))(get y)", "DDD");
// 配列の配列に対するforeachとarrayとの組み合わせ
assertEval("(let x ('AAA' 'BBB')('CCC' 'DDD') ) (foreach e x (get (array e 1)) )", "BBBDDD");
// 配列の配列に対するforeachとarrayによるreplaceの繰り返し
assertEval("(let x ('AAA' 'XXX')('BBB' 'YYY')('CCC' 'ZZZ') )(set z 'AAAWWWBBBWWWCCC') (foreach e x (set z (replace z (array e 0) (array e 1) ) )) (get z)"
, "XXXWWWYYYWWWZZZ");
// arrayとみなして任意の位置の要素を設定できる。
// これは高速化のために参照透明性を壊すので、他のオブジェクトから参照されているとそちらも更新されてしまうので注意すること。
assertEval("(set x 'AAA' 'BBB' 'CCC')(setarray x 2 'DDD')(get x)", "AAABBBDDD");
// 配列の任意の位置に設定できる。サイズを超えた場合は配列は自動的に拡張される。
// 拡張された部分はすべて #null になる。
assertEval("(setarray x 10 'DDD')(setarray x 3 'CCC')(get x)", "CCCDDD");
assertEval("(setarray x 10 'DDD')(setarray x 3 'CCC')(array x 3)", "CCC");
assertEval("(setarray x 10 'DDD')(setarray x 3 'CCC')(array x 2)", "#null");
// 配列の大きさはlengthによって取得できる。
assertEval("(let x 'AAA' 'BBB' 'CCC')(length x)", "3");
assertEval("(setarray x 10 'DDD')(setarray x 3 'CCC')(length x)", "11");
// eqは中身を評価して文字列レベルでの一致を調べる。
// 一致すれば#true , 一致しなければ #falseが返る。
// neqはeqと逆条件。not equalの略
assertEval("(eq 'ABC' 'ABC')", "#true");
assertEval("(eq 'ABC' 'CDE')", "#false");
assertEval("(set x 'ABC')(eq x 'ABC')", "#true");
assertEval("(set x 'ABC')(eq x 'CDE')", "#false");
assertEval("(set x 'CDE')(neq x 'CDE')", "#false");
assertEval("(set x 'ABC')(neq x 'CDE')", "#true");
// ifは#trueならば直後の式を評価する。さもなくば、その次の式を評価する。
// そして評価した式を副作用として返す
assertEval("(set x 'AAA')(if (eq x 'AAA') 'TRUE' 'FALSE')", "TRUE");
assertEval("(set x 'AAA')(if (neq x 'AAA') 'TRUE' 'FALSE')", "FALSE");
assertEval("(set x 'AAA')(if (eq x 'AAA') (set x 'BBB') (set x 'CCC'))(get x)", "BBB");
// ifの式が偽で、else相当句がなければ、if式の値として#falseが返る。
assertEval("(set x 'AAA')(if (eq x 'BBB') 'TRUE')", "#false");
// ifは3項演算子と等価。
assertEval("(set x 5)(set y (if (eq x 5) 1 2))(get y)", "1");
assertEval("(set x 3)(set y (if (eq x 5) 1 2))(get y)", "2");
// or演算子はどちらかが#trueならば#true
assertEval("(or 'AAA' (eq 1 1))", "#true");
// and演算子は両方が#trueのときだけ#true
assertEval("(and 'AAA' (eq 1 1))", "#false");
assertEval("(and (eq 5 5)(eq 3 3) )", "#true");
// whileは条件式が #true の間、回り続ける
// (while cond exp)
// 5回ループでyに毎回3ずつ足せば合計は15になっているはず。
assertEval("(set x 0)(set y 0)(while (neq x 5) ((set x (add x 1)) (set y (add y 3))) (get y)", "15");
// forで回すことが出来る。
// for ループカウンタ 開始値 終了値 評価する式
// ダウンカウントはしない。
assertEval("(set z '')(for x 0 9 (addto z x) ) (get z)", "0123456789");
// ループカウンタが1ずつ減るfor
assertEval("(set z '')(downfor x 9 0 (set z (get z x)) ) (get z)", "9876543210");
// 大小比較
// gt = greater than : < , lt = less than : >
// ge = greater equal : <= , le = less or equal : <=
assertEval("(lt 1 2)", "#true");
assertEval("(lt 2 1)", "#false");
assertEval("(lt 1 1)", "#false");
assertEval("(gt 1 2)", "#false");
assertEval("(gt 2 1)", "#true");
assertEval("(gt 1 1)", "#false");
assertEval("(ge 1 2)", "#false");
assertEval("(ge 2 1)", "#true");
assertEval("(ge 1 1)", "#true");
assertEval("(le 1 2)", "#true");
assertEval("(le 2 1)", "#false");
assertEval("(le 1 1)", "#true");
// car,cdr。これはLISPのものに準拠する。
assertEval("(let x 1 2 3)(car x)", "1");
assertEval("(let x 1 2 3)(cdr x)", "23");
assertEval("(let x 1)(cdr x)", "#null");
assertEval("(let x 1 2 3)(print (cdr x))", "'2' '3'");
// evalは変数に代入された式を評価する。
assertEval("(let x (print 'ABC'))(eval x)", "'ABC'");
assertEval("(let x (set y 'ABC')(addto y 'DEF'))(eval x)", "ABCDEF");
assertEval("(set x 3)(let z (add x 4))(set y (if (eq x 5) 1 (eval z)))(get y)", "7");
// 括弧として (){}[]《》【】〔〕〈〉[]が使える。同じ種類の括弧が対応している必要がある。
// すべて () と等価。
assertEval("(set y {add 1 2}) (let x {print 'ABC' y})[eval x]", "'ABC' '3'");
// func命令は関数を定義する。これだけなら、evalしているのと変わらない。
assertEval("(func F (print 'ABC')) (F)", "'ABC'");
// @で始まるのはローカル変数。関数のなかでだけ使える。
// また、特に、@0,@1,…は関数に渡されたパラメータ。
assertEval("(func F (get @0 'と' @1)) (F 'ABC' 'DEF')", "ABCとDEF");
assertEval("(func F [get @0 'と' @1]) (let p1 'ABC' 'DEF') (let p2 'GHI') (F p1 p2)", "ABCDEFとGHI");
assertEval("(func F [print @0 'と' @1]) (let p1 ('ABC' 'DEF'))(let p2 'GHI') (F p1 p2)", "('ABC' 'DEF') 'と' 'GHI'");
// ':'で終わる変数名に見えるものはラベル。変数名と':'との間にスペースなどを入れるのは不可。
// break + ラベルでそのlabelのステートメントを抜ける。(JavaScript風)
assertEval("(label1: while '#true' { (print 'ABC')(break label1) } ) ", "'ABC'");
// break + ラベルでいくつでも外のスコープまで抜けることが出来る。さながら例外処理である。
assertEval("(label0: while '#true' { label1: while '#true' { (print 'ABC')(break label0) }} ) ", "'ABC'");
// forなど制御構文もbreakで抜けることが出来る。
assertEval("(label0: for x 0 5 { (if (eq x 3) (break label0) ) (addto y x) }) (get y)", "012");
// foreverは永久ループ。breakと組み合わるといいかも。
assertEval("(label0: (set x 0) [forever { (if (eq x 3) (break label0) ) (set x (add x 1)) (addto y x) }]) (get y)", "123");
// switch~case。
// (switch val { val1 exp1 } { val2 exp2 } ... {default exp0 } )のように書く。
// val==val1ならexp1が実行される。このときswitchの値は、exp1の評価後の値になる。
// val==val2ならexp2が実行される。このときswitchの値は、exp2の評価後の値になる。
// valがそれより前のcaseにおいてどれとも合致していない場合は、default節のexp0が評価され、これがswitchの値となる。
assertEval("(set x 1)(get {switch x (1 'ABC') (2 'CDE')(3 (mul 2 3) ) } )", "ABC");
assertEval("(set x 2)(get {switch x (1 'ABC') (2 'CDE')(3 (mul 2 3) ) } )", "CDE");
assertEval("(set x 3)(get {switch x (1 'ABC') (2 'CDE')(3 (mul 2 3) ) } )", "6");
assertEval("(set x 5)(get {switch x (1 'ABC') (2 'CDE')(3 (mul 2 3) )(default 'ディフォルト値') } )", "ディフォルト値");
assertEval("(set x 2)(set y 2)(get {switch x (1 'ABC') (y 'CDE')(3 (mul 2 3) ) } )", "CDE");
// import命令は、ファイルから読み込み、それをS式として式の評価として返す。
// 読み込むファイルは、//% の行がLISP式として評価されるバージョン
// test1.cppには" (set x 'ABC')(set y 'DEF')(addto x y) "と書かれているとすると…
assertEval("(eval (include 'test.lsp'))", "ABCDEF");
// include命令は、ファイルから読み込み、それをS式として式の評価として返す。
// importはC/C++/C#のソースファイルを対象とするため、LISP行は、 //% で開始している必要があり、
// それ以外の行は、文字列として扱われる。
// 生成元 : Debug/test.cpp → 生成先 : Debug/testout.cpp
// それぞれのファイルを見ると、何か参考になるかも。
assertEval("(eval (import 'test.cpp'))", "..done");
/* // ↓等価
using (var file = new StreamReader("test.cpp"))
{
var exp = new ConvSExp().import(file);
new Lisp().eval(exp);
}
*/
<<
* 更新履歴
2009.09.05 Version 1.00 公開
2009.11.24 Version 1.04 公開