Last update 1999/08/07
(C)平山直之
無断転載は禁止、リンクはフリー
誤字脱字の指摘は歓迎
はい、また例によって行き当たりばったりな企画です。
といっても、相当長い間私の心の大きな部分を占めていた問題ではあります。
それは言語の処理系の必要性についての問題です。
ゲーム制作、特にRPG・アドベンチャーなどの「シナリオ」の重要性が高いものを作るのに必要不可欠なものに、「イベントスクリプトの処理系」というものがあります。ネットでもこうした「イベントスクリプトの処理系」について考える人が少なくないのも、こうした必要性の表われと言えるでしょう。
しかし、こうした処理系は、それぞれのプログラマが独自の文法でプロジェクトごとに作り直しているのが現状です。これが、コードの再利用、ひいては「気楽にゲームを作る」上での大きな障害になっているは間違いありません。シナリオスクリプトドリブンのRPG、ADV、シミュレーションRPGなどを作るためにはこうした処理系が必ず必要になるのにも関わらず、これらは「作り方を知らない人には決して易しいものではない」「作り方を知ってる人にとってもかなり面倒なものである」からです。
現状がこうなのはなぜか? それには、以下のような理由があると考えています。
- 必要な機能を備えた言語仕様の策定は、単純な問題ではない。多くの言語に触れたプログラマでなければ、汎用に耐える言語文法は作れない(選べない)。
- Cなどの言語しか知らない人は、言語プログラミングは難しいと思いがち。
- シナリオ実行時のイベントプログラムはどんなプロジェクトでも自分で書かなくてはならないため、共通化は困難と考えがち。
それぞれについて考えてみましょう。
言語仕様の策定
ここに、GNUの主宰者リチャード・ストールマンの「なぜTclではだめか? 」という論文(の和訳)があります(続編しか見つかりませんでした。情報求ム)。Tclとは、この当時UNIXでかなり普及していたスクリプト言語ですが、ストールマンは(政治的な思惑もあったかもしれませんが)これを否定した上で、GNUプロジェクトに共通の機能拡張言語の必要性を訴えました。
この論文で触れられている論題の中でここで重要なのは、大まかに言って以下の2つです。
- 機能拡張言語は決して簡易言語ではない。簡易言語では不十分である。
- アプリケーション間で共通化が必要。
簡易言語の簡易言語たるゆえんは、特定の用途を想定することによって、その用途への簡便性(短い記述で大きな効果を得られる)を提供する代わりに、汎用性を犠牲にしていることです。
そのため、簡易言語では、その簡易言語の想定する応用範囲を超えるとかえって大きな労力が必要になることが多々あります。プログラミングとは期せずともえてしてそうなるもので、それがゲームのスクリプトであればなおさらそうであると言ってよいでしょう。
また、特定の応用範囲を想定した簡易言語では、いくらその言語に慣れてもそのノウハウが未来に生きません。その言語を想定した開発環境のサポートも困難です。結局、そんなスクリプト言語をプロジェクトごとに作るくらいだったらハードコーディングする方がまし、とさえ言えるのです。
言語プログラミングの難しさ
言語プログラミングは難しいと一般に思われています。それはある意味正しく、ある意味間違っています。
実際、一から自分で作るとしたら、(慣れが大きく関わることではありますが)それは難しく時間のかかることであると言わざるを得ないでしょう。
しかしそれだけに、世の中にはそれをサポートするツールもたくさんあるのです。それを使えば、かなりの時間を節約することができます。
たとえば、以下の条項を守ることによって、本来難しい言語プログラミングの難しさを軽減することができます。
- lexを使う。
- yaccを使う。
- 簡単な文法を選ぶ。
- 文法を削る。
- インプリメントに使う言語に、木構造を簡単に扱える(できればファーストクラスオブジェクトとして)言語を選ぶ。
まずlexについて。これは、プログラマが書いたスクリプトから、「字句解析器」と呼ばれるテキストをトークン(単語のような解析上の単位)に分割するプログラムを自動的に生成するツールです。字句解析は手書きしてもそれほど難しいものではないのですが、とても面倒でメンテナンスの大変なものです。そこで手書きをする代わりにこれを使えば、正規表現や「状態」を使って解析を簡単に行うことができます。
次にyaccについて。これは「構文解析器」と呼ばれる、トークンの並びを分析して「構文木」と呼ばれる内部構造を作るためのツールです。これもlexと同じような理由で、手書きするよりもyaccスクリプトを書いてyaccに処理させる方が開発効率・メンテナンスの面で数段効率的です。
次は「簡単な文法」について。処理系を作る上では説明するまでもありませんね。それだけでなく、文法を簡単にすると、将来的にもメリットがあります。構文が簡単だと、(たとえばクロスリファレンスツールなどの)開発環境を手軽に開発できるようになるのです。これは決して侮れないメリットと言ってよいでしょう。
さらに「文法を削る」について。巷間の言語には、シンタックスシュガーと呼ばれる、本来必要でないがコーディングの効率化のために設けられている特別な文法が備えられていることが多くあります。こうしたシンタックスシュガーはサポートツールの作成などにも邪魔になることが多いので、削ってしまうのも手です。
最後にインプリメントに使う言語について。先ほど説明したように、言語の解析とはテキストを木構造に変換すること(そこまでできれば後は難しくない)と言っても過言ではないので、それをサポートする機能が言語にあれば、実装は格段にたやすくなります。ただし、実行効率などとの兼ね合いもあるので、あまりマイナーな言語を選ぶのも考えものでしょう。
共通化の難しさ
答えからいってしまいましょう。現在では、ウィンドウプログラミングの一般化による「コールバック」という概念の普及や、DLLのような共有ライブラリの存在もあって、共通化はさほど難しくはありません。重要なのは、そうした外部からのアクセスに耐えられるように、「入力はファイルからだけ」とか「外部関数を呼び出す機能がない」などといった制限をつけないようにするだけです。
共通の処理系
以上のような観点から、ゲームに関わらず使える(つまりPDS扱い、オープンソース・オープンアーキテクチャの)処理系は必要であり、かつ実現可能であると私は考えました。そこでそれを作ろうというのが、このプロジェクトの基本理念です。
なぜschemeを選んだか? これは、私がこれまでに見たことのある「完成した言語」の中で、もっともシンプルで美しい文法を持ち、柔軟な構造を扱えることです。
schemeはlispの派生物であり、膨れ上がってしまったCommonLispの反省から注意深く最小限の機能だけを残した、洗練された言語です。
schemeのプログラムを見れば分かると思いますが、ほとんど「ソース=木構造」と言っても過言ではないほどシンプルな構文になっており、ほかの言語に見られるような紛れがありません(これはschemeと言うよりlisp全般の特徴です)。したがって構文解析も簡単であり、サポートツールも簡単に作れます。また「リストが言語組み込みの機能である」とか、「データとコードの表記形式が同じ」など、さまざまな柔軟な機能を持っています。いろいろな言語の文法を見てきましたが、はっきりいってこれほどゲーム向きの言語もありません。
欠点としては、カッコだらけでソースが読みにくいということがあげられます。これはよくいわれるように「最初だけ」なので特に心配要りません。またemacsが使えるなら、オートインデントが働くため、ほかのエディタでCのコードを書くより書きやすいくらいです(逆に言うと、カッコの対応を示してくれないエディタだと若干ストレスがあります)。
schemeについてはここに入門があるようなので、見てみるとよいでしょう。
開発環境
さて能書きはこのくらいにして、実際に実装を始めましょう。
まず以下のような開発環境を想定しました。
- 文法にはschemeを採用。
- 字句解析にはflexを使う。
- 構文解析にはbisonを使う。
- 実装言語にはC++を使う。
schemeを選んだ理由については前に述べました。
字句解析・構文解析にflex・bison(GNUのlex・yacc)を用いるのは、単純に使うのと使わないのとではまったく労力が違うからです。昔と違って、今ではflexとbisonで作られたプログラムも商用利用OK(らしい)ので、このままで特に問題ないと思われますが、schemeの場合字句解析・構文解析ともにそんなに難しいものではない(事実かつて私はすべて手書きでschemeより面倒なCommonLispのparser・lexerを書いたことがある)ので、様子を見て書き直そうと思います。
さらに実装言語にC++を使うことについて。C++では、リスト・文字列こそファーストクラスオブジェクトではありませんが、現在ではSTL、stringの存在でほぼそれに準ずる簡便さを得られます。これを利用することによって、相当な量の労力を削減できることが予想できます。
flex・bisonはcygnus win32のものを、C++コンパイラにはVC++を使います。
字句解析
前述のように、字句解析はflexを使います。
実際には以下のようなスクリプトを書きました。
// 第一部 %{ #include <io.h> #include <FlexLexer.h> #include "parser.cpp.h" // やむなし #define isatty _isatty int mylineno = 0; %} // 第二部 letter [\x00-\xff] kanji ([\x80-\xff]{letter}) mark [\!\$\%\&\*\+\-\.\/\:\<\=\>\?\@\^\_~] alpha [A-Za-z] digit [0-9] ws [ \t\n] com \; cr \n escape \\ escaped ({escape}{letter}) dquote \"ident ({kanji}|{mark}|{alpha}|{digit})* comment {com}[^\n]*{cr} string {dquote}([^{escape}{dquote}]|{escaped})*{dquote} %% // 第三部 {cr} mylineno++; {ws}+ /* skip blanks and tabs */ {comment} /* skip comment */ \. {return DOT;} {ident} {return IDENTIFIER;} {string} {return STRING;} \( {return LPAR;} \) {return RPAR;} \# {return SHARP;} \' {return QUOTE;} %% // 第四部 int yywrap(void) { return 1; }簡単に説明しておきましょう。ちなみに、この色の部分は便宜上つけたもので実際のスクリプトにはありません。
flexは、このスクリプトを読んで、字句解析を行うC(C++)ソースを出力します。
第一部は、flexが出力するソースの先頭にそのまま挿入される部分です。一般にインクルード指定などを行います。
第二部では別名の定義を行っています。マクロ定義のようなものだと思って下さい。実際には、左側の名前が別の場所に現れると右側のものに置き換えられます。右側の文字列は正規表現です。
第三部が重要な部分です。基本的には、左側がマッチする文字列、右側がマッチしたときの動作を表しています。ここでは、トークンの種類を構文解析器に渡すだけなので、{return xxx;}の羅列になっています。
第四部も出力に追加される部分で、flexが出力しない(が必要とする)コードを書くのが主な用途になります。
これ以上の詳しい情報については、この辺りを参照してください。
構文解析、の前にデータ構造
次は構文解析、といきたいところですが、その前に構文木をどういう構造で持つかを考えなければなりません。というのも、yaccがやってくれるのは特別のパターンにマッチングすることだけだからです。
ここでは、多態性を前提としてC++で設計することにしました。
構文木のようなデータ構造を作る場合、構文木のすべてのノードをある基底クラス(ここではIObjectとしましょう)から派生するのはまず当然と言ってよいですが、その基底クラスにどのようなインターフェイスを持たせるかについては、選択される戦略は大きく分けて以下の2つになります。
- Compositeデザインパターン。
- dynamic_castを前提とした最小限のインターフェイス。
まずCompositeデザインパターンについて。Compositeデザインパターンでは、たとえば「そのオブジェクトが文字列であると仮定してその文字列としての値を取得したいとき」を想定して基底クラスに「文字列としての値を取得する」メソッドを置きます。もちろん一時が万事で、シンボルとして、リストとしての値を取得するためにもそれ専用のメソッドを追加していくことになります。
このパターンの長所は、使うときにいちいちダウンキャストを伴う型判定が必要ないことです。逆にこのパターンの欠点は、「閉じた再利用性」がないことです。IObjectから新しい派生クラスを導入するときに、その派生クラスに必要なインターフェイスをIObjectでも仮想関数宣言しなければならないからです。
一方のdynamic_castを前提とした最小限のインターフェイスについて。こちらでは、IObjectから派生した新しいクラスを導入するときにもIObjectに手を加える必要がないため、「閉じた再利用」が可能です。しかしながら、実際にこのクラスのオブジェクトに働きかけたいときには、型を判定してダウンキャストを行わなければなりません。
ではこの場合、どちらを選ぶのがよいのでしょうか。
lispのインタプリタを設計した経験から言えば、この場合は圧倒的に前者です。基本的に、システム側から見て未知の派生クラスが後から追加されるということがほぼありえないため、「閉じた再利用性」について考える必要がありません。また、言語処理システムでは「ユーザのミス」が日常茶飯事で、オブジェクトにたいして本来そのオブジェクトに対して行われてはならない操作が行われることが珍しくありませんが、その「ミス」を確実にトラップしなければなりません。その際に、「基底クラスの仮想関数でthrowをインプリメントしておき、オーバーライドされていない限りそれが実行される」ようにしておく戦術が大変有効なのです。こうしておくことで、呼び出す側でオブジェクトの型チェックをする必要がほぼなくなります。問答無用で操作を行ってしまえば、間違っているときには的確に例外が射出されるからです。
というわけで作ったのが以下のヘッダファイルです。後々オブジェクトの永続化をサポートすることを考えて、いくつかのクラスが実装されていますが(DataBase,IClass,IWriter,IReaderなど)、基本はIObjectとその派生クラスです。
#if !defined(VSCHEME_HPP) #define VSCHEME_HPP #include <string> #include <map> #include <set> #include <vector> #include <list> namespace vscheme { /*============================================================================ * * class IClass * * AbstractFactory * *==========================================================================*/ class IObject; class IWriter; class IReader; class IClass { public: IClass(void){} virtual ~IClass(void){} virtual IClass* clone(void)const=0; virtual IObject* create_object(void)const=0; #if 0 virtual void write_object(IWriter&,IObject*)const=0; virtual void read_object (IReader&,IObject*)const=0; #endif virtual const char* get_name(void)const=0; }; template <class T> class Class : public IClass { public: Class(void){} ~Class(void){} IClass* clone(void)const { return new Class<T>; } IObject* create_object(void)const { return new T; } #if 0 void write_object(IWriter& w,IObject* o)const { w << (*((T*)o)); } void read_object (IReader& r,IObject* o)const { r >> (*((T*)o)); } #endif const char* get_name(void)const { return typeid(T).name(); } }; /*============================================================================ * * class DataBase * * オブジェクト管理 * *==========================================================================*/ template <class BaseObject> struct GarbageFinder { public: bool operator()(BaseObject* o) { return !o->get_mark(); } }; template <class BaseObject> class DataBase { public: DataBase(void){} virtual ~DataBase(void){} void register_class(IClass* c) { vClasses.push_back(c->clone()); } template <class T> BaseObject* create(Class<T>& c) { BaseObject* o=c.create_object(); vObjects.insert(o); return o; } void destroy(BaseObject* o) { vObjects.remove(o); o->destroy(); } void clear(void) { foreach(std::set<BaseObject*>,vObjects,i){ (*i)->destroy(); } vObjects.clear(); vNames.clear(); } void name_object(BaseObject* o,const std::string& s) { assert(o!=NULL); vNames[s]=o; } BaseObject* get_named_object(const std::string& s) { return vNames[s]; } void collect_garbage(void) { // すべてのオブジェクトのマークを消去 {foreach(std::set<BaseObject*>,vObjects,i){ (*i)->set_mark(false); }} // 辞書からたどれるオブジェクトにマークをつける {foreach(std::map<std::string,BaseObject*>,vNames,i){ if((*i).second!=NULL){ (*i).second->mark(); } }} // マークの付いてないオブジェクトを削除 std::remove_if( vObjects.begin(), vObjects.end(), GarbageFinder<BaseObject>()); } void save_to_stream(const std::ostream&){} void load_from_stream(const std::istream&){} std::set<BaseObject*>::const_iterator begin(void)const {return vObjects.begin();} std::set<BaseObject*>::const_iterator end (void)const {return vObjects.end();} private: std::set<IClass*> vClasses; std::set<BaseObject*> vObjects; std::map<std::string,BaseObject*> vNames; }; /*============================================================================ * * class IObject * * オブジェクトインターフェイス * *==========================================================================*/ class Context; class IObject { public: IObject(void){} virtual ~IObject(void){} virtual void destroy(void) {delete this;} virtual void set_mark(bool f) {vMark=f;} virtual bool get_mark(void) {return vMark;} virtual void mark(void) =0; virtual void print(std::ostream&) =0; virtual void eval(Context&) {} // as boolean virtual void set_boolean(bool) {throw 1;} virtual bool get_boolean(void) {throw 1;} // as symbol virtual void set_name(const std::string& s) {throw 1;} virtual std::string get_name(void) {throw 1;} virtual void set_bind(IObject*) {throw 1;} virtual IObject* get_bind(IObject*) {throw 1;} // as cons virtual void set_car(IObject*) {throw 1;} virtual void set_cdr(IObject*) {throw 1;} virtual IObject* get_car(void) {throw 1;} virtual IObject* get_cdr(void) {throw 1;} // as continuation virtual void set_next(IObject*) {throw 1;} virtual void set_sexp(IObject*) {throw 1;} virtual IObject* get_next(void) {throw 1;} virtual IObject* get_sexp(void) {throw 1;} // as string virtual void set_string(const std::string& s) {throw 1;} virtual std::string get_string(void) {throw 1;} // as vector virtual void set_elements(IObject*) {throw 1;} virtual IObject* get_elements(void) {throw 1;} private: bool vMark; }; /*============================================================================ * * class Nil * * Nil [()のこと] * *==========================================================================*/ class Nil : public IObject { public: Nil(void){} ~Nil(void){} void mark(void); void print(std::ostream&); private: }; /*============================================================================ * * class Boolean * * 真偽値 (#f,#t) * *==========================================================================*/ class Boolean : public IObject { public: Boolean(void){} ~Boolean(void){} void mark(void); void print(std::ostream&); // as boolean void set_boolean(bool); bool get_boolean(void); private: bool vBoolean; }; /*============================================================================ * * class Symbol * * シンボル * *==========================================================================*/ class Symbol : public IObject { public: Symbol(void){vBind=NULL;} ~Symbol(void){} void mark(void); void print(std::ostream&); // as symbol void set_name(const std::string& s) ; std::string get_name(void) ; void set_bind(IObject*) ; IObject* get_bind(void) ; private: std::string vName; IObject* vBind; }; /*============================================================================ * * class Cons * * コンス * *==========================================================================*/ class Cons : public IObject { public: Cons(void){vCar=NULL;vCdr=NULL;} ~Cons(void){} void mark(void); void print(std::ostream&); // as cons void set_car(IObject*) ; void set_cdr(IObject*) ; IObject* get_car(void) ; IObject* get_cdr(void) ; private: IObject* vCar; IObject* vCdr; }; /*============================================================================ * * class Continuation * * 継続 * *==========================================================================*/ class Continuation : public IObject { public: Continuation(void){vNext=NULL;vSExp=NULL;} ~Continuation(void){} void mark(void); void print(std::ostream&); // as continuation void set_next(IObject*) ; void set_sexp(IObject*) ; IObject* get_next(void) ; IObject* get_sexp(void) ; private: IObject* vNext; IObject* vSExp; }; /*============================================================================ * * class String * * 文字列 * *==========================================================================*/ class String : public IObject { public: String(void){} ~String(void){} void mark(void); void print(std::ostream&); // as string void set_string(const std::string& s); std::string get_string(void) ; private: std::string vString; }; /*============================================================================ * * class Vector * * ベクタ * *==========================================================================*/ class Vector : public IObject { public: Vector(void){} ~Vector(void){} void mark(void); void print(std::ostream&); // as vector void set_elements(IObject*) ; IObject* get_elements(void) ; private: std::vector<IObject*> vVector; }; /*============================================================================ * * class Context * * 評価コンテキスト * *==========================================================================*/ class Context { public: DataBase<IObject> db; IObject* nil; IObject* quote; IObject* b_false; IObject* b_true; }; } extern int yylex(); extern void yyerror(char*); extern int yyparse(); extern char* yytext; extern int yyleng; extern vscheme::Context* pc; #endif構文解析
さて、いよいよ本番の構文解析です。基本的にはyaccに任せるだけですし、lispの構文はとても簡単なものなので、それほど難しいことはありません。
とは言ってもyaccが分からないと分からないと思いますが、yaccの説明をするのはさすがに大変なので、市販の書籍などを参考にしてください(残念ながら適当な日本語webリソースは見つかりませんでした)。
今回は、以下のようなbisonスクリプトを書きました。ポイントは、構文パターンにマッチする毎にオブジェクトを生成し、それを接続することです。
pcはグローバル変数で、parse_contextの略と考えてください。
pc->db.create(vscheme::Class<vscheme::Symbol>())は、
new vscheme::Symbolと等価であると考えて問題ありません。
%{ #include <iostream> #include <malloc.h> #include "VScheme.hpp" #define alloca _alloca #define YYSTYPE vscheme::IObject* %} %token IDENTIFIER %token STRING %token LPAR %token RPAR %token SHARP %token QUOTE %token DOT %% toplevel : /* 空 */ | toplevel sexp { $2->print(std::cout); } ; sexp: IDENTIFIER { vscheme::IObject* o= pc->db.create(vscheme::Class<vscheme::Symbol>()); o->set_name(yytext); $$=o; } | STRING { vscheme::IObject* o= pc->db.create(vscheme::Class<vscheme::String>()); o->set_string(std::string(yytext+1,yyleng-2)); $$=o; } | LPAR slist { $$=$2; } | SHARP LPAR slist { vscheme::IObject* o= pc->db.create(vscheme::Class<vscheme::Vector>()); o->set_elements($3); $$=o; } | QUOTE sexp { vscheme::IObject* o= pc->db.create(vscheme::Class<vscheme::Cons>()); o->set_car(pc->quote); o->set_cdr($2); $$=o; } ; slist: DOT sexp RPAR { $$=$2; } | RPAR { $$=pc->nil; } | sexp slist { vscheme::IObject* o= pc->db.create(vscheme::Class<vscheme::Cons>()); o->set_car($1); o->set_cdr($2); $$=o; } ; %%
オブジェクトの実装
最後はオブジェクトのインターフェイスの実装、およびパーサの呼び出し部分です。
この辺はまだいい加減ですが、必要に応じて書き換えればよいでしょう。
// VScheme.cpp : コンソール アプリケーション用のエントリ ポイントの定義 // #include "stdafx.h" #include "VScheme.hpp" void yyerror(char* p) { printf("error: %s\n",p); } int main(int argc, char* argv[]) { pc->nil =pc->db.create(vscheme::Class<vscheme::Nil>()); pc->quote =pc->db.create(vscheme::Class<vscheme::Symbol>()); pc->quote->set_name("quote"); pc->b_true =pc->db.create(vscheme::Class<vscheme::Boolean>()); pc->b_true->set_boolean(true); pc->b_false =pc->db.create(vscheme::Class<vscheme::Boolean>()); pc->b_false->set_boolean(false); return yyparse(); } vscheme::Context parse_context; vscheme::Context* pc=&parse_context; namespace vscheme { #define MARK_OBJECT(x) if((x)!=NULL && !(x)->get_mark())(x)->mark() /*============================================================================ * * class Nil * * * *==========================================================================*/ // Nil ここから //**************************************************************** // mark void Nil::mark(void) { } //**************************************************************** // print void Nil::print(std::ostream& os) { os << "()"; } // Nil ここまで /*============================================================================ * * class Boolean * * 真偽値 (#f,#t) * *==========================================================================*/ // Boolean ここから //**************************************************************** // mark void Boolean::mark(void) { } //**************************************************************** // print void Boolean::print(std::ostream& os) { if(vBoolean){ os << "#t"; } else { os << "#f"; } } //**************************************************************** // set_boolean void Boolean::set_boolean(bool f) { vBoolean=f; } //**************************************************************** // get_boolean bool Boolean::get_boolean(void) { return vBoolean; } // Boolean ここまで /*============================================================================ * * class Symbol * * * *==========================================================================*/ // Symbol ここから //**************************************************************** // mark void Symbol::mark(void) { set_mark(true); MARK_OBJECT(vBind); } //**************************************************************** // print void Symbol::print(std::ostream& os) { os << vName; } //**************************************************************** // set_name void Symbol::set_name(const std::string& s) { vName=s; } //**************************************************************** // get_name std::string Symbol::get_name(void) { return vName; } //**************************************************************** // set_bind void Symbol::set_bind(IObject* o) { vBind=o; } //**************************************************************** // get_bind IObject* Symbol::get_bind(void) { return vBind; } // Symbol ここまで /*============================================================================ * * class Cons * * * *==========================================================================*/ // Cons ここから //**************************************************************** // mark void Cons::mark(void) { set_mark(true); MARK_OBJECT(vCar); MARK_OBJECT(vCdr); } //**************************************************************** // print void Cons::print(std::ostream& os) { os << '('; vCar->print(os); IObject* p=vCdr; while(typeid(*p)==typeid(Cons)){ os << ' '; p->get_car()->print(os); p=p->get_cdr(); } if(typeid(*p)==typeid(Nil)){ os << ')'; } else { os << " . "; p->print(os); } } //**************************************************************** // set_car void Cons::set_car(IObject* o) { vCar=o; } //**************************************************************** // set_cdr void Cons::set_cdr(IObject* o) { vCdr=o; } //**************************************************************** // get_car IObject* Cons::get_car(void) { return vCar; } //**************************************************************** // get_cdr IObject* Cons::get_cdr(void) { return vCdr; } // Cons ここまで /*============================================================================ * * class Continuation * * * *==========================================================================*/ // Continuation ここから //**************************************************************** // mark void Continuation::mark(void) { set_mark(true); MARK_OBJECT(vNext); MARK_OBJECT(vSExp); } //**************************************************************** // print void Continuation::print(std::ostream& os) { os << "#<continuation>"; } //**************************************************************** // set_next void Continuation::set_next(IObject* o) { vNext=o; } //**************************************************************** // set_sexp void Continuation::set_sexp(IObject* o) { vSExp=o; } //**************************************************************** // get_next IObject* Continuation::get_next(void) { return vNext; } //**************************************************************** // get_sexp IObject* Continuation::get_sexp(void) { return vSExp; } // Continuation ここまで /*============================================================================ * * class String * * * *==========================================================================*/ // String ここから //**************************************************************** // mark void String::mark(void) { set_mark(true); } //**************************************************************** // print void String::print(std::ostream& os) { os << '"' << vString << '"'; } //**************************************************************** // set void String::set_string(const std::string& s) { vString=s; } //**************************************************************** // get std::string String::get_string(void) { return vString; } // String ここまで /*============================================================================ * * class Vector * * * *==========================================================================*/ // Vector ここから //**************************************************************** // mark void Vector::mark(void) { set_mark(true); for(std::vector<vscheme::IObject*>::iterator i=vVector.begin(); i!=vVector.end(); i++){ MARK_OBJECT(*i); } } //**************************************************************** // print void Vector::print(std::ostream& os) { os << "#<vector>"; } //**************************************************************** // set void Vector::set_elements(IObject* o) { } //**************************************************************** // get IObject* Vector::get_elements(void) { return NULL; } // Vector ここまで } // namespace vscheme
ここで一休憩
さて、lispのソースファイルを読んで構文木の構築までできるようになりました。
- コードとデータの構文上の違いがない
- 標準的にリスト・木構造を持っている
といった特徴をlisp(scheme)は持っていますので、ここまでの実装ですでにこれらの機能を利用した柔軟なデータベースとしての使い方ができます。
まだこの構文木を実際に評価する部分が残っていますが、lispの「評価」はとても単純なので、ここまででできれば全部できたも同然です。後はゆっくり、オブジェクトの未実装の部分を実装すればいいだけです。
……と言いたいところですが、残念ながらこれには「従来のlispならば」という但し書きがつきます。
schemeの場合、継続というとても大事なオブジェクトがあります。これが対して意味のないおまけ的な機能であれば無視すれば済むことなのですが、これがschemeの構造上かなり大事な上に、ひょっとしたらゲームプログラミングにも革新的なほど役立つかも知れない機能なのです。
一応頭の中にはどうすればよいかという大まかな設計は立っている(その設計は前出のソースにも一部現れている)ので、次回(と言っても多分次が最後)はこれを考えるところから始めたいと思います。
今回のソースをまとめたものをアップロードしておきます。
(C) 1998 Naoyuki Hirayama. All rights reserved.