メンバページ: Go

(続)やさしい Lisp の作り方 by Java


このページは、「やさしい Lisp 処理系」の続編です。非定期にやっていこうと思います。長い目で付き合ってあげてください。


- 更新記録 -
1日目 正式パッケージと Eclipse によるリファクタリング
2日目 エラー処理
3日目 文字列型とリード関数
4日目 ロード

1日目 --- 正式パッケージと Eclipse によるリファクタリング

パッケージ名は "funado" で定義してきました。 今日はこのパッケージ名を正式なパッケージ名として登録します。

このときにパッケージ名のリファクタリングをする必要がありますが、 今日はEclipse を使って機械的に行います。

パッケージ名の世界唯一性を保障しないと、名前の衝突が起こる可能性 があります。唯一性を保障するために URI を使います。OKIソフトウェアの場合で あれば、okisoft.co.jp ですので、パッケージ名は jp.co.okisoft.* になります。 当エンジニアリングソリューションセンタであれば、その略称名である esc を付けて、jp.co.okisoft.esc.* になります。

Lisp 処理系のパッケージ名を jp.co.okisoft.esc.funadoLisp にします。 もちろん、これは勝手に決めるのではなく、各組織で衝突しないように 一般的に登録方法が決められていて、登録手段が提供されています。 今回は正式に登録することにより、提供しているソースコードの パッケージ名の衝突を避けることができるようになります。

次に実際のパッケージ名の変更のリファクタリングを行います。 ここを手動で行うと、修正し忘れなどのバグを埋め込む原因にも なりますので、なるべく機械的な方法で行います。

Eclipseによって、このリファクタリングを行います。 Eclipse の紹介は ここにありますので、 興味のある方は参照してください。

Eclipse によるパッケージ名の変更は容易で、パッケージ名 "funado" で右クリックして、「リファクタリング」−「名前の変更」 を行うだけです。以下のウィンドウが表示されますので、 jp.co.okisoft.esc.funadoLisp を入力します。

これだけで、すべての pakcgae 文、imports 文などが機械的に変更 されます。

今日は正式なパッケージ名とそのリファクタリングとして 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 ループに 制御を戻しています。

以下にエラークラスを紹介します。

// 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 が多重継承をサポートしていれば、 もっと自然な実装になっています。)

次にエラーをスローする側を見てみます。クラス 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 の引数は文字列ですので、文字列型を定義します。

今までは文字列型は用意していませんでした。まずは文字列型に対する 入力用メソッド 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 に します。

このクラスでコンストラクタや印字 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 を登録し、そのために文字列型を 導入しました。

See you again !!

このページトップへ

4日目 --- ロード

今日は関数 load ロードを作成します。

load は lisp のソースファイルを引数に与えて、そのファイルをメモリ上に S 式として読み込み、その S 式を評価する関数です。ファイルの最後までこれを繰り返します。

ファイルからのリーダ read-from-string とエヴァリュエータ eval で実装できます。

以下にこの 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 式が完結していないときに、次の行を読むように実装していません。これは実務的に必要な機能ですが、ここではまだ実装していません。

次にこの関数を 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);
...

以下に 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 !!

このページトップへ





Copyright (c) 2003 OKI Software Co., Ltd.