メンバページ: Go
(続)やさしい Lisp の作り方 by Java
このページは、「やさしい Lisp 処理系」の続編です。非定期にやっていこうと思います。長い目で付き合ってあげてください。
- A: 何で Lisp の続編なんですか
- G: それはじゃ、色々と反響もあったからじゃ
- A: ネタは続くのかな・・・
| 1日目 | 正式パッケージと Eclipse によるリファクタリング |
| 2日目 | エラー処理 |
| 3日目 | 文字列型とリード関数 |
| 4日目 | ロード |
1日目 --- 正式パッケージと Eclipse によるリファクタリング
パッケージ名は "funado" で定義してきました。 今日はこのパッケージ名を正式なパッケージ名として登録します。
このときにパッケージ名のリファクタリングをする必要がありますが、 今日はEclipse を使って機械的に行います。
パッケージ名の世界唯一性を保障しないと、名前の衝突が起こる可能性 があります。唯一性を保障するために URI を使います。沖ソフトウェアの場合で あれば、okisoft.co.jp ですので、パッケージ名は jp.co.okisoft.* になります。 当エンジニアリングソリューションセンタであれば、その略称名である esc を付けて、jp.co.okisoft.esc.* になります。
Lisp 処理系のパッケージ名を jp.co.okisoft.esc.funadoLisp にします。 もちろん、これは勝手に決めるのではなく、各組織で衝突しないように 一般的に登録方法が決められていて、登録手段が提供されています。 今回は正式に登録することにより、提供しているソースコードの パッケージ名の衝突を避けることができるようになります。
- A 君: なんで長ったらしい名前にするんですか。面倒!
- G 君: まぁ、仕方がないんじゃ。不特定多数で使用される可能性 があるときには特に仕方がないんじゃ。
- A 君: ふーん。短名にエイリアスしましょう。
次に実際のパッケージ名の変更のリファクタリングを行います。 ここを手動で行うと、修正し忘れなどのバグを埋め込む原因にも なりますので、なるべく機械的な方法で行います。
- A 君: そりゃ、そうですね。
Eclipseによって、このリファクタリングを行います。 Eclipse の紹介は ここにありますので、 興味のある方は参照してください。
Eclipse によるパッケージ名の変更は容易で、パッケージ名 "funado" で右クリックして、「リファクタリング」−「名前の変更」 を行うだけです。以下のウィンドウが表示されますので、 jp.co.okisoft.esc.funadoLisp を入力します。
これだけで、すべての pakcgae 文、imports 文などが機械的に変更 されます。
- A 君: なんだ、簡単じゃん。もしかして今日は1分だけ、作業時間は?
- G 君: げげげ、実はそうじゃ。
- A 君: 1分だけの報告でこんだけ書いていたの?。
今日は正式なパッケージ名とそのリファクタリングとして Eclipse を 使う方法を示しました。明日は Lisp 処理系のエラー処理について報告します。
See you again !!
2日目 --- エラー処理
今日はエラー処理を真面目に対応するようにします。
今までは、例えば、変数の未束縛のエラーが起こっても、単に
System.out.println("Error: " + car.serialize() + " is not defined.");
return Nil.NIL;
をしていただけでした。つまり、エラーメッセージを直接印字して NIL を値として返していました。
そこでこのエラーを処理するルーチンを組み込むようにします。 まず、read-eval-print のメインループでこのエラーをキャッチする ようにします。以下にこのループがある Lisp クラスの変更を紹介します。
while (true) {
try {
lisp.pw.print("Lisp> "); lisp.pw.flush();
sexp = lisp.read.read();
if (sexp.serialize().equals("QUIT")) break;
sexp = lisp.eval.eval(sexp);
sexp.print(lisp.pw);
lisp.pw.println(); lisp.pw.flush();
} catch (Error e) {
e.print(lisp.pw);
lisp.pw.println(); lisp.pw.flush();
}
}
ここでキャッチしている Error は、jp.co.okisoft.esc.funadoLisp.Error で新たに定義したクラス Error です。 このエラークラスの情報を print して、read-eval-print ループに 制御を戻しています。
- A 君: エラークラスにやっと手を出しましたか。
- G 君: そうじゃ。でもキャッチ&スローを定義してからが本当じゃが。
以下にエラークラスを紹介します。
// funado.Lisp 2004 Copyright (C) GOMI Hiroshi
package jp.co.okisoft.esc.funadoLisp;
import java.io.*;
/**
* Error エラーオブジェクト
* エラーになったときに、エラーが生成される
* @author Go
*
*/
public class Error extends Exception implements Sexp {
java.lang.String message; // エラーメッセージ
java.lang.String errorInformation; // エラー補助情報
int errorNo; // エラー番号
static final java.lang.String errorMessages[] = {
"Undefined Error", // 0
"Unbound Variable", // 1
"Undefined Function", // 2
"Not Function", // 3
"Not Symbol", // 4
};
/**
* エラー生成(未定義エラー)
*/
Error(){
new Error(0, "");
}
/**
* エラー生成
*/
Error(int err){
new Error(err, "");
}
/**
* エラー生成
*/
Error(int err, java.lang.String errorInfo){
message = getMessage(err);
errorInformation = errorInfo;
errorNo = err;
}
/**
* getMessage
* エラー番号からエラーメッセージを返す
*/
java.lang.String getMessage(int err){
return errorMessages[err];
}
/**
* Error のプリント
*/
public void print(PrintWriter pw) throws Exception {
pw.print(serialize());
}
/**
* Error のシリアライズ(文字列化)
*/
public java.lang.String serialize(){
return "Error: " + message + " --- " + errorInformation;
}
}
クラス Error は Exception のサブクラスとして定義し、Sexp の 実装になっています。(今回も Java が多重継承をサポートしていれば、 もっと自然な実装になっています。)
- A 君: エラークラスの直接の親に Exception ですか。
- G 君: Throwable のサブクラスとして Lisp のスローじゃ。
- A 君: いやそういう意味じゃなくて、直接的過ぎるんですよ。
- G 君: うーむ、継承モデルじゃなくて委譲にするべきだったかな。
次にエラーをスローする側を見てみます。クラス eval で 未束縛や未定義のエラーをスローしています。以下にそのコードを 示します。
System.out.println("Error: " + form.serialize() + " is not unbound.");
return Nil.NIL;
--->
throw new Error(Lib.UNBOUND, form.serialize());
このようにエラーを出す箇所を上記のように Error を毎回生成して、 それをスローしています。なお、定数 Lib.UNBOUND は Lib クラスで定義し ています。 以下に実行の様子を示します。
Lisp> qwe Error: Unbound Variable --- QWE Lisp>
Error#print()で上記のメッセージを表示しています。 今はまだユーザ解放のエラーキャッチを提供していませんが、 その準備はこれでできたことになります。
今日はエラー処理を作成しました。
See you again !!
3日目 --- 文字列型とリード関数
今日は Reader.java で既に定義しているメソッド read と read-from-string を lisp の関数として、登録します。これにより、read 関連が lisp 処理系から使えるようになります。
しかし、この前に read-from-string の引数は文字列ですので、文字列型を定義します。
- A 君: ということは、今まで文字列が使えなかったんですね。
- G 君: そうじゃ、Lisp に文字列は不要じゃ。
- A 君: え、ちょっと過激、いや面倒じゃないんですか。使えないと
- G 君: まぁ、そうじゃが・・・シンボルを1文字1文字に分割できれば、まぁ使えるんじゃ。
- A 君: と言っても作るんですね。やっぱり不便だったんですね。
今までは文字列型は用意していませんでした。まずは文字列型に対する 入力用メソッド read と出力用メソッド print を作成します。 read は Reader.java の中で定義します。入力中に " (ダブルクォート) が 出現すれば、文字列生成用関数 makeString を呼び出します。 以下にこのプログラムを掲載します。
--- Reader.java
Sexp getSexp() throws IOException {
...
case '\"': return makeString(); // " (ダブルクォート)は文字列生成へ
/**
* 文字列の読み込み makeString
*/
Sexp makeString() throws IOException {
StringBuffer str = new StringBuffer();
while (indexOfLine < lineLength) {
getChar();
if (ch == '\"') break;
str.append(ch);
}
LispString lispStr = new LispString("" + str);
return lispStr;
}
次に文字列のクラスを作成します。名前を LispString に します。
- A 君: なんで、LispString なんですか。String じゃなくて。
- G 君: まぁ、大人の判断じゃ。
- A 君: 面倒だったんでしょう。java,lang.String とするのを忘れて、単に String としていたところがあったんでしょう。
- G 君: ぐぐぐ。
このクラスでコンストラクタや印字 print、文字列化 serialize を作成します。 以下に新規ファイル LispString.java を掲載します。
--- LispString.java (新規追加ファイル)
// funado.Lisp 2003, 2004 Copyright (C) GOMI Hiroshi
package jp.co.okisoft.esc.funadoLisp;
import java.io.*;
/**
* String class is defined for matching Lisp type system
*/
public class LispString implements Sexp {
private String value;
// コンストラクタ群
public LispString() { value = ""; }
public LispString(String string) { value = string; }
// valueOf
public String valueOf() { return value; }
/**
* 数値の印字
*/
public void print(PrintWriter pw) throws Exception {
pw.write(serialize()); // 文字列に変換してからプリント
}
/**
* 数値のシリアライズ
*/
public java.lang.String serialize() { return "\"" + valueOf() +"\""; }
}
次に、関数 read と read-from-string を登録します。 以下でこれらの関数を登録する Function.java のその部分を掲載します。
--- Function.java
/**
* READ
*/
class Read extends Function {
public Sexp fun(List arguments, int argNum) throws Exception{
return (Sexp)Lisp.lisp.read.read();
}
}
/**
* READ-FROM-STRING
*/
class ReadFromString extends Function {
public Sexp fun(List arguments, int argNum) throws Exception{
Sexp arg1 = eval.eval(arguments.car);
return (Sexp)Lisp.lisp.read.readFromString(((LispString)arg1).valueOf());
}
}
上記の登録で使用している静的変数 Lisp を Lisp.java で定義します。 これは今までローカル変数で使用していた同名の変数 Lisp をグローバル に変更します。
--- Lisp.java
public class Lisp {
static Lisp lisp; // Lisp 処理系(自分自身)
...
public static void main(java.lang.String[] args) {
// Lisp lisp; // Lisp 処理系 (削除、上記へ移動)
これで登録が完了しました。以下にこれらの関数の実行結果を示します。
---実行結果--- funado.Lisp if quit from system, then you type 'quit'. Lisp> (setq str (read)) "abc" <---- キーボード入力 "abc" Lisp> str "abc" Lisp> (read-from-string "'abc") (QUOTE ABC)
上記は、関数 read を実行して、キーボードから "abc" を打鍵しました。 その結果は文字列の "abc" になり、その値を評価すると、その値も "abc" 自身になり、それを表示しています。
次に関数 read-from-string を引数 "'abc" で実行します。 結果は 'abc を評価して、(QUOTE ABC) となります。
今日は関数 read と read-from-string を登録し、そのために文字列型を 導入しました。
- A 君: 本当は read と read-from-string だけを軽く登録するだけだったんでしょう。
- G 君: 実は文字列型を作っていなかったことを忘れておった。
- A 君: まぁ、おかげで、やっと文字列が使用できるようになったんですね。
- G 君: 怪我の功名かの。
See you again !!
4日目 --- ロード
今日は関数 load ロードを作成します。
load は lisp のソースファイルを引数に与えて、そのファイルをメモリ上に S 式として読み込み、その S 式を評価する関数です。ファイルの最後までこれを繰り返します。
ファイルからのリーダ read-from-string とエヴァリュエータ eval で実装できます。
- A 君: 前日の read-from-string はこのためだったんですね。
- G 君: 実はそうじゃ。関数ロードを作成したいと思って、read-from-string を登録しようと思ったんじゃ。
- G 君: そうしたら、文字列型を作っておらず、それを作ったんじゃ。
- A 君: なんか、泥縄ですね。これも2日間も使っているし、まぁ、いいか。
以下にこの load を Reader.java に実装したものを示します。
--- Reader.java
/**
* LOAD
*/
public Sexp load(java.lang.String fileName) throws Exception{
BufferedReader oldBr = br;
try {
FileInputStream in = new FileInputStream(new java.io.File(fileName));
br = new BufferedReader(new InputStreamReader(in));
init();
for (;;) {
java.lang.String str = br.readLine();
if (str == null) break;
Sexp sexp = readFromString(str);
Sexp ret = Lisp.lisp.eval.eval(sexp);
}
br.close();
br = oldBr;
return T.T;
} catch (IOException e) {
br = oldBr;
return Nil.NIL;
}
}
load はファイルをオープンしてバッファドリーダに読み込みの準備をします。 for 文の中で br.readLine() により1行1行 Java の文字列として読み込みます。これを readFromString により、S 式として読み込みます。
次に eval によって、この S 式を評価します。これを EOF (ファイルの終了) まで 繰り返します。
ここで注意ですが、1行の文字列で読み込み、それを S 式としてリードしているだけ ですので、1行で S 式が完結していないときに、次の行を読むように実装していません。これは実務的に必要な機能ですが、ここではまだ実装していません。
- A 君: ちょっと、駄目じゃん。
- G 君: まぁ、改行を入れなければいいんじゃ。
- A 君: うーん。
次にこの関数を lisp から使用できるように登録します。 これを以下に示します。
--- Function.java
/**
* LOAD
*/
class Load extends Function {
public Sexp fun(List arguments, int argNum) throws Exception{
Sexp arg1 = eval.eval(arguments.car);
java.lang.String fileName = ((LispString)arg1).valueOf();
return Lisp.lisp.read.load(fileName);
}
}
次に lisp 起動時に、初期化ファイルとして lisp のソースファイルをロードするようにしてみます。
今までは Lisp.java の初期化部分に直接、ベンチマーク用関数 tak を定義していましたが、それを外部ファイル start.lsp に格納して、lisp の起動時にそれをロードするようにします。これを以下に示します。
--- Lisp.java
final static java.lang.String startLsp = "start.lsp"; // 起動ファイル
------- Lisp#main
...
try {
lisp = new Lisp();
// start.lsp の読み込み
lisp.read.load(startLsp);
...
- A 君: やっぱり、tak が入るんですか。
- A 君: 噂では tak 専用言語とも言われていますが。
- G 君: いやいや、そんなことはない!はずじゃ、きっと、たぶん。
- G 君: 後は S 式関数を一杯作っていくんじゃ。
- A 君: それで実装できなければ、システム関数を Java で作ると・・・
- G 君: そーじゃ。遅延実装じゃ、なるべく Java での作成は後回しにするんじゃ、メタ巡回的じゃ。
- A 君: なんか、言い訳のように聞こえますが。まぁ、いいか。
以下に lisp の起動と "start.lsp" からロードされた tak の定義と、lisp 上から関数 load を使用して "test.lsp" をロードした結果を示します。
funado.Lisp if quit from system, then you type 'quit'. Lisp> tak (LAMBDA (X Y Z) (IF (>= Y X) Z (TAK (TAK (- X 1) Y Z) (TAK (- Y 1) Z X) (TAK (- Z 1) X Y)))) Lisp> (load "test.lsp") T Lisp> test (LAMBDA (X Y) (CONS X Y)) Lisp>
今日は関数 load を作成しました。start.lsp に S 式関数を増やすことで、lisp を豊富化することができるようになりました。
See you again !!