余計なお世話シリーズ。
星田さんがLand of LispのANSI Common Lispで書かれたゲームをRacketに移植しようと頑張っている。
凄い良い試みである。
勉強が出来るヤツは違うのである。
でも、ちょっと待てよ、って思った。
いや、止めないんだけど(笑)。
冷静に考えてみると、ANSI Common LispとRacketを始めたばっかの人が両者の関数を調べて比較しつつ書く、ってのは負担が大きいんじゃないか、と。
そこで、余計なお世話と知りつつ、取り敢えず「魔法使いのアドベンチャー」と題したゲームで使われてる関数の対応表を作ってみようか、と思いたち、作ってみた。
多分これを参照してくれたら上手い具合出来るんじゃないかな、とか思う。
実際、carだのcdr等と言ったLispの総初期からある関数名はANSI Common LispとSchemeでは被ってる事が多い。
しかし「それ以外」だと結構違うんだ。
登場時の話をすると、ANSI Common Lispの方がSchemeより新しい。
ただし、「Lispを再定義しようとしたScheme」では、全面的にそれまで各種Lisp方言の「何となく使われていた」関数名を見直し、どっちかっつーとモダンな名称を付けるようにしている。
一方、ANSI Common Lispの場合、「各Lisp方言の総まとめ」的な仕様を目指した為に、それまで使われてきた名称を無批判で取り込んだような名称群になってる・・・・・・どう考えても「名前をマジメに考えず」に、やっつけで付けたような名前が多いのだ。
例えばprin1とかprincとか。どう見ても「雑な名付け」としか思えないだろ(笑)?どっかで誰かがこの実装を必要になって、「取り敢えずやっつけで名前を付けました」感がビンビンである(笑)。
ANSI Common Lispの関数名ってそんなんばっかなんだよ(苦笑)。
しかも時代別な総博覧状態である。
ANSI Common Lispの短い関数名は「コンピュータの文字数制限が厳しかった時代の」古参の関数であり、一方、クソ長い名前は「コンピュータの文字数制限が厳しくなくなって」、かつ「IDE的な、自動補完環境が整った時代に生まれた」比較的新しい関数である。
もうメチャクチャなのだ(笑)。
実はANSI Common Lispでは、carやcdrなんかの「パッと見意味が分からない名前」を廃止しようか、と言った議論もあったらしい。もっと分かりやすいfirstとかrestにしようぜ、とか言った話があった、とか。
ところがどっこい、古参Lisperにこの案は総スカンを喰らうんだな(笑)。
まぁ大体、古参はどの時代でもロクでもなく、
「俺様がこれに慣れてるからこれにしてくれ」
と言うのがまかり通るわけだ。
しょーがねぇので、折衷案として、firstとcarは両方仕様になる事になった。restとcdrもだ。
実際、ANSI Common Lispではfirstやrestを実のトコ推奨してるらしい。でも誰も従わないのだ(笑)。
それで考えると「実用Common Lisp」を書いたPeter Norvigは偉いんだよ。
事実この本にはcarやcdrは出てこない。全部firstやrestになっとる。
個人的にLispプログラムをどう書いてるのか、ってのは知らんが、少なくとも自分が書いた教科書では「今からLispを学ぶ」New Comerに「正しいプログラムの書き方」を教えようと、一貫してるわけ。物凄く「正しいスタイル」で本全体を律しようとしてる。
それが分かるから、彼の本は信用出来るのだ。
(C99対応!とかC11対応!とか謳ってる本で、ANSI Cみたいな書き方してるCの入門書しかない、ってCのクッソpoorな状況よりよほどマシだと分かるだろ?)
余談だが、例えば登場がかなり新しいClojureなんかでは作者が業を煮やしたのか、確かcarとかcdrと言う関数名は廃止してた筈。うろ覚えだけど。もう全部firstとrestだ。モダンだ。カッコイイ。末尾再帰最適化がねぇのは許せないけどな(笑)。
まぁ、そんなこんなで、実際、Racket/SchemeとANSI Common Lispの「関数の対応」を探るのは結構骨なんだよね・・・・・・過去に僕もやったから良く分かるわけよ(笑)。
と言うわけで、上の表を役立ててくれれば嬉しいな、と言うお話。
ちなみに、ANSI Common Lispの関数名とSchemeの関数名の目立つ違いと、両者間の移植のある程度のコツに対して纏めておくけど、
- ANSI Common Lispではやっつけで付けたような名称が多い。反面、Schemeはモダンで直感的で比較的分かりやすい関数名になっている。
- Schemeでは真偽値を返す関数、いわゆる「述語」と呼ばれる関数に対しては名前の最後は「?」で終わる、と言う命名規約に従っている。反面、ANSI Common Lispではそんなルールはない。良心的な関数だと末尾が「p」か「-p」で終わってるモノもあるが絶対、ではない。しかもpは意味が分かりづらい(predicate=述語、の略)。
- Schemeでは破壊的変更を伴う関数や特殊形式の名称は末尾を「!」で終わる、と言った命名規約に従っている。一方、ANSI Common Lispではそんな命名規約もないし、同じような機能で破壊的変更があるもの、ないもの、の両者は全く似ても似つかないような名称になってる事が多い。
- ANSI Common Lispの関数ないしはマクロは、総称的(引数のデータ型を問わない)モノが多い。従って、対応すべきSchemeの関数は、適用すべきデータ型に従って、別々の関数として定義されてる事が多い。
- ANSI Common Lispだと真はシンボルT、偽はNIL、またはそれを意味する空リスト('())になっている。一方、Schemeは真が#tだが偽は#fであり、偽と空リストは別物である。従って、ANSI Common Lispでは(not '())は真だが、Schemeでは(not '())は偽である。Schemeだと#fが唯一の偽であって、「それ以外」は全部真だから、だ。
- ANSI Common Lispの偽はNILで空リストの為、コード上でNILが現れた時に、真偽値を表してるのか、空リストを表してるのかパッと見では全く分からない。これはSchemeにコードを移植しようとした時の障壁となり得て、「機械的に翻訳する」のを許さない場合がある。
- ANSI Common Lispでは(car '())と(cdr '())はエラーを起こさない。意外とANSI Common Lispは数学的で、リストを「集合」として扱えるように設計されていて、空集合から何も取り出せない事、残りは何も無いこと、がキチンと表現されている。一方、Schemeでは(car '())と(cdr '())は単にエラーとなる。
- 前述の性質の為、再帰で空リストが絡む時、単純移植が上手く行かない場合がある。その場合、ANSI Common Lispではnull判定、Schemeではnull?判定を終了条件として付加したり、あるいは削る必要性が出てくる。
ってのがガイドライン、っつーか注意事項かな。
ちなみに、以下は余談。
RacketはUTF-8対応なので、シンボルに日本語を使ったり、関数名も日本語にしたりできます。
従って、「魔法使いのアドベンチャー」は日本語で出来るゲームにする事が可能です。
ちょこちょこと日本語対応にする為に改造してるけど、古き良き、日本語入力のテキストアドベンチャーゲームを作る事も出来るんだよ、と言う事を言っておきましょう。
ソースコードはコチラに。
参考まで。