JavaLisp Vol.4  完成、そしてグラフィック


さて、この連載もいよいよ最終回となりました。
ベーシックなLispインタプリタとしての完成とグラフィック関数の追加を、あわせて一気に紹介しましょう。誌面も限られているのでテンポよくいきたいと思います。


■オブジェクト指向にエバリュエータを再構築する


●Function型オブジェクトの導入

S式の評価機構(エバリュエータ)の実現方法として、エバリュエータをそのままクラス化した前回までのEvaluator.javaを廃止し、逆に各S式にeval()メソッドを追加して、S式に「自分で自分を評価」させるのが今回紹介するバージョンです。
まずS式がシンボルの場合は単純です。シンボルのeval()メソッドがよばれると、シンボルが自分で環境を調べ、自分と同じシンボル名に束縛されている値を返します。未束縛の場合はもちろんExceptionです。またシンボル以外のS式(数、文字列、T、Nil)の場合は自分自身を値として返します。(リスト1、2

リスト1 Symbolクラスのeval()メソッド
--------------------------------------------------------------------------

  public SExpression eval(Hashtable environment) throws Exception {

    //TとNilは定数なので各々のオブジェクトを返す
    if(this.getStr().equals("T")) return new T();
    else if(this.getStr().equals("NIL")) return new Nil();

    //一般の変数は環境を調べて束縛されている値(=S式)を返す
    else {
      SExpression value = (SExpression) environment.get(this.getStr());
      if(value != null) {
        return value;
      else
        throw new Exception("error: unbound variable - "+ this.getStr());
    }
  }

---------------------------------------------------------------------------


リスト2 Integer, Real, String, Nil, T クラスのeval()メソッド
--------------------------------------------------------------------
  public SExpression eval(Hashtable environment) throws Exception {
    return this;    //自分を返す
  }

--------------------------------------------------------------------

これに対して一見考え方が難しいのが、リスト(フォーム)の場合のeval()メソッドです。フォームの場合は先頭の関数に応じて、その種類だけの処理の分岐が必要なので、その処理ごとにeval()メソッドの内部を書き足していったら以前のEvaluator.javaを丸ごと持ち込むことになってしまいます。
そこで関数に応じた分岐をシンプルに処理するための方法論が、今回新しく登場する「Function型」というオブジェクトです。
図1Function型オブジェクトの継承構造
    quilt.function.*

 

リスト3 Listクラスのeval()メソッド
--------------------------------------------------------------------

  public SExpression eval(Hashtable environment) throws Exception {

    //自分の先頭要素から、その手続きオブジェクトを得て
    Function f;
    try {
      f = ((SExpression)this.get(0)).function(environment);
    }
    catch(Exception e) { throw e; }
    //リストの残りの要素を「引数リスト」としてまとめて
    List arguments = new List();
    for(int i=1; i<this.size();i++) {
      arguments.add(this.get(i));
    }
    //手続きオブジェクトへ引数リストを適用する
    try {
      return f.apply(arguments, environment);
    }
    catch(Exception e) { throw e; }
  }

---------------------------------------------------------------------

Function型オブジェクトは「引き数が入力されたら何かの処理をして結果を出力する」という、文字どおり関数の役割をするオブジェクトで、Quote、Setf、First等の各種の関数は、すべてFunctionのサブクラスと考えるのです。     そしてFunctionオブジェクトは、外部からの入力(引数のS式)を受け付ける「関数の入口」が必要ですから、それはFunctionクラスのapply()メソッドとして準備します。メソッドの返り値が関数からの「出口」です。

また、スペシャルフォーム系の関数とそれ以外の関数(引数を事前に評価しない/する)を分けるには、apply()の引数の前処理が異なる別々の抽象クラスを間にはさみます。そして前処理の実装は抽象クラス内で済ませておけば、実際の各関数クラスでは、その関数固有の処理だけを実装すればよいことになります。こうして様々な関数がFunction型のポリモルフィズムに吸収される
わけです。

その実装にあたっては、今回はprocess()というapply()の下働きメソッドを抽象定義して、そのprocess()をサブクラスごとに書く方法をとりました。
しかしこのあたりは人によって好みが分かれる部分かもしれません。例として一番シンプルなQuoteクラスを掲載します。
(リスト

ここにみられるように

・引数の文型チェック
・実際の手続き


という処理がどの関数でも基本になります。

また今回は、前回までの関数に加えて、述語判定や比較、分岐やループなどの制御構造、四則演算などをひととおり作成し、必要最小限のプリミティブを揃えました。各関数についてはソースを御覧いただければと思います。<注>
なお、これらFunction型のオブジェクトのことを、以下の文中ではしばしば「手続きオブジェクト」とよびます。LISPの文脈的にはそのほうがわかりやすいのではないでしょうか。

<欄外注>
誌面の都合で掲載していませんが、本誌ホームページhttp://www.gihyo.co.jp/javapress/からダウンロードできます。



●Lambdaクラス


ところで、Function型の各クラスのうち、ひとつだけ毛色が異なるのがLambdaクラスです。(リスト)他のプリミティブではコンストラクタに引数がなくメンバー変数も持たないのに対し、Lambdaの場合はnewで渡されたラムダ式に応じて

(1)新しい環境
(2)束縛変数リスト
(3)ラムダ式本体


をメンバー変数として内部に保持するため、ラムダ式がカプセル化された感じがよくわかると思います。このLambdaの実行時の働きはシンプルで、process()から入った実引数を新しい環境に束縛して、その環境をもとにラムダ式本体のS式のapply()をよび直すだけです。

リスト4 Quote.java
--------------------------------------------------------------------------------------
public final class Quote extends SpecialFunction {

  public Quote() {
  }
  public SExpression process(List arguments, Hashtable environment) throws Exception {
    //文型チェック...引数は1つ
    if(arguments.size() <1) throw new Exception("error: too few arguments");
    if(arguments.size() >1) throw new Exception("error: too many arguments");
    //手続き
    return (SExpression) arguments.get(0);      //その引数を返すだけ
  }
}

--------------------------------------------------------------------------------------


●S式から手続きオブジェクトを得る

以上の準備を済ませてeval()に話題を戻るすと、Listクラスのeval()メソッドは(リスト「自分の先頭のS式からその手続きオブジェクトを得て、残りのS式を引数のリストとしてまとめ、手続きオブジェクトのapply()メソッドに渡す」とシンプルに記述できます。Function型の導入で、処理の本質だけを書きながら関数に応じた処理が可能になりました。
ところで、ここでいきなり何気なく、先頭のS式からその手続きオブジェクトを得てと書いたのですが、これはどうすればいいのでしょう?
結論からいえば、もう一種類別のメソッドをS式に追加します。どのようなメソッドかといえば、「自分に対応するFunctionオブジェクトを生成して返す」というfunction()メソッドです。
つまり自分で自分を評価させるeval()メソッドと同じ考え方で、もちろん最上位のSExpressionで抽象定義して各S式クラスごとに実装します。
続けてクラスごとのfunction()をながめていきましょう

<欄外注>一般的なjavaの命名法の流儀に従えばgetFunction()などのメソッド名にしたほうがわかりやすいかもしれませんが、なるべくLisp風の概念を強調するためこう命名しました。
リスト5 Lambda.java (エラーチェックは掲載省略)
----------------------------------------------------------------------------------------

public class Lambda extends NormalFunction {

  Hashtable env = null;			//新しい環境
  List params = new List();	//束縛変数リスト
  List body = new List(); 		//ラムダ式本体

  public Lambda(List lambdaForm) {
    //新しい環境を作り
    env = new Hashtable();
    //束縛変数を新しい環境でNilに束縛
	List paramList = (List)lambdaForm.get(1);
    for(int i=0; i<paramList.size(); i++) {
        SExpression param = (SExpression) paramList.get(i);
        params.add(param);
        env.put(param.getStr(), new Nil());
    }
    //ラムダ式の本体を記憶
    for(int i=2; i<lambdaForm.size(); i++) {	//本体は複数のフォームかもしれない
        body.add((SExpression) lambdaForm.get(i));
    }
  }

  public SExpression process(List arguments,Hashtable environment) throws Exception {
    //新しい環境内の束縛変数に実引数を束縛
    for(int i=0; i<params.size(); i++){
      env.put(((SExpression) params.get(i)).getStr(), arguments.get(i));
    }
    //ラムダ式の本体を新しい環境で評価する
    SExpression rtn = new Nil();
    for(int i=0; i<body.size(); i++) {		//本体は複数のフォームかもしれない
      try {
        rtn = ((SExpression) body.get(i)).eval(env);
      }
      catch(Exception e) { throw e; }
    }
    return rtn;  //最後の値を返す
  }
}

----------------------------------------------------------------------------------------


●S式のfunction()メソッド


まずSymbolクラスのfunction()メソッドは、シンボル名にあわせた手続きオブジェクトを生成して返します。(リスト6-(1)<注>
しかしそのシンボルに対応するプリミティブがない場合は、ユーザがDefunした関数名かどうを調べます。しかし見つかった場合に得られるのはラムダ式(Function型ではない)なので、まだfunction()の戻り値にはなりません。

そのためシンボルはこのラムダ式のfunction()をよんで手続きオブジェクトを得ようとします。(リスト6-(2))ラムダ式のfunction()は後述のListクラスの場合に含まれますので、Symbolクラスのfunction()はこれで終わりです。



リスト6 Symbolクラスのfunction()メソッド
----------------------------------------------------------------------------------------
  public Function function(Hashtable environment) throws Exception {

    //定義済みのプリミティブの場合------------@
      if(this.getStr().equals("QUOTE")) return new Quote();    //Quoteオブジェクトを返す
      else if(this.getStr().equals("SETF")) return new Setf(); 	//Setfオブジェクトを返す
      else if(this.getStr().equals("DEFUN")) return new Defun(); //Defunオブジェクトを返す
		(以下、プロミティブ数だけ続く--省略)

    //それ以外はユーザがDefunした手続きを環境に探しにゆき
    else {
      SExpression lambdaForm = (SExpression) environment.get(this.getStr());
      if(lambdaForm==null) {			//見つからなけば未定義エラー
        throw new Exception("error: unbound function - "+ this.getStr());	
      }
      try { 			//とりだしたラムダ式のfunction()をよぶ------------A
        return lambdaForm.function(environment);
      }
      catch(Exception e) { throw e; }
    }
  }

----------------------------------------------------------------------------------------

<欄外注>ここでは説明を簡単にするために、シンボルに対応するプリミティブをnewしていますが、実際は逐一動的に生成する必要はなく、プリミティブはstaticに用意します。本稿の実際のソースではプリミティブを納めたハッシュを保持するクラス(Primitives.java)を用意して、そのハッシュからプリミティブを取り出します。


次にシンボルやリスト以外のS式におけるfunction()メソッドでは、すべてExceptionを返します。たとえばNilは手続きになりえないので対応する手続きオブジェクトを持ちません。数や文字列も同様です。(リスト


リスト7 Integer, Real, String, Nil, Tクラスのfunction()メソッド
------------------------------------------------------------------

  public Function function(Hashtable environment) throws Exception{
    throw new Exception("error: bad function - " + getStr());
  }

------------------------------------------------------------------

続けてListクラスのfunction()メソッドをみてみましょう。リストに対してfunction()が呼ばれる可能性があるのは、

(1)上記のように、ユーザがDefunしたラムダ式が環境からとりだされた時
(2)フォームの先頭(関数の位置)に直接ラムダ式が書いてある時
(3)APPLY文などの「関数引数」をとるフォームで、引数の中にFUNCTION式が出現した時

の3パターンです。すなわちラムダ式かファンクション式にケースが限られるので、それらについて処理を書けば十分です。まずラムダ式の場合は、自分のラムダ式に応じたLambdaオブジェクト(前述)を作って返すだけです。(リスト8-(1))次にファンクション式--例えば(FUNCTION FIRST)--は第2要素の手続きオブジェクトを返す構文です。よって第2要素のfunction()を呼びだすだけで、それは結局、上記のいずれかにケースがおさまります。(リスト8-(2)


リスト8 Listクラスのfunction()メソッド (エラーチェックは掲載省略)
--------------------------------------------------------------------

  public Function function(Hashtable environment) throws Exception{

    //自分がLambda式なら------------(1)
    if(((SExpression) this.get(0)).getStr().equals("LAMBDA")){
      return new Lambda(this);			//Lambdaオブジェクトを生成する
    }
    //自分がFunction式なら-----------(2)
    else if(((SExpression) this.get(0)).getStr().equals("FUNCTION")){
      try {				//第2要素のfinction()をよぶ
        return ((SExpression) this.get(1)).function(environment);
      }
      catch(Exception e) { throw e; }
    }
    //それ以外のリストは手続きを表現したフォームではない
    else
      throw new Exception("error: bad function - "+ this.getStr());
  }

--------------------------------------------------------------------


以上で、S式におけるeval()およびfunction()メソッド--すなわち新しいエバリュエータ--の実装はすべて完了です。



●エバリュエータの実行

これで役者はそろいました。あとは評価の実行方法です。前回まではLispクラスから、評価したいS式をEvaloatorに渡していました。

evaluator.eval(form, environment);

これをS式自身のeval()メソッドを呼ぶよう、

form.eval(environment); と変更するだけでOKです。後はそのS式のネストにしたがってeval()が連鎖してゆきます。 以上でエバリュエータの変更は終了です。 S式のクラス構成がそのまま評価の分岐につながり、LISPのシンプルな本質がそのまま表現できたように筆者は感じるのですが、
読者の皆さんはいかが感じられますか? NILと空リストの同一性について ところで、今回作成したFunctionオブジェクトのうち一番苦労したのは、実は一見簡単そうなListpやAtomなどの、データ型を判定する
述語です。基本的にinstanceof演算子で判定すればいいのですが、問題になったのが「NILと空リストは同一」というLISPの言語仕様で した。そこでS式全体の継承構造を図2のように改めました。 ●図2 S式オブジェクトの継承構造       quilt.datatype.*
これならばNilはサイズが0のListでもあり、またアトムのinstanceofもとれるため、「NILは空リスト」
という方向は解決します。
しかし、どうがんばっても解決できないのは「空リストはNIL」の方向です。しかたがないので、各関数の引数チェックや述語判定などの場面で、
本来ならば「instanceof NIL」と判定をするところを「リスト&&サイズが0」と付け焼き刃的に対応するしかありませんでした。(ソースではisVoidList(S式)という
メソッドを抽象クラスに実装し各関数で利用しています)

このあたりを美しくやろうと思ったら、前号の「リストのCDRをとれない」問題も含めて、結局はコンスセルの概念をベースにしたS式クラスでないと
難しいような気も少々してきています。


■環境、クロージャ、スコープルール


●EnvironmentクラスとClosureクラスの導入
では次の話題として、宿題になっているクロージャの追加にうつりましょう。その準備として、まずは"環境"を少々改造します。
いままでの環境にはHashtableを利用していましたが、ここで新たに、Hashtableを継承したEnvironmentクラスを導入します。(リスト9

リスト9 Environment.java
--------------------------------------------
import java.util.*;

public class Environment extends Hashtable {

  protected Environment parent = null;

  public Environment() {
    super();
  }

  public Environment(Environment env) {
    super();
    parent = env;
 }

  public void setParent(Environment env) {
    parent = env;
 }

  public Object put(Object key, Object value) {
    if(this.containsKey(key) || (parent==null))
      return super.put(key,value);
    else
      return parent.put(key,value);
  }

  public Object get(Object key) {
    Object ret = super.get(key);
    if(ret!=null)
      return  ret;
    else if(parent != null)
      return parent.get(key);
    else
      return null;
  }
}

--------------------------------------------
Hashtableから少しだけ機能を拡大したのは、自分と同じ型のオブジェクトへの参照を保持できる点です。
いわばリンクリストなどと同じ考え方でHashtableのリンクを作るわけです。
そしてput()/get()がよばれると、自分にそのキーがあればput()/get()をおこない、なければ参照先のput()/get()へ処理をリレーします。
もし参照先がなければ自分のみが対象になりますのでHashtableと挙動は変わりません。これで環境の改造は終わりです。
そしていよいよ導入するのが、クロージャを実装したClosureクラスです。これはソースを直接見ていただくのがてっとり早いでしょう。
しかしなんとも、意外とあっさりしています。(リスト10

リスト10 Closure.java
-------------------------------------------------------

public class Closure extends Lambda {

  public Closure(List lambdaForm, Environment parent) {
    super(lambdaForm); 
    env.setParent(parent);   //外の環境をとりこむ
  }
}

-------------------------------------------------------


ClosureクラスはLambdaクラスを継承していて、その違いはコンストラタの引数と内部の環境だけです。しかしこれが強力で、Closureの場合は
引数で渡された環境の参照をLambda内の環境へ追加するため、渡された元の環境を調べることができます。

これで先のEnvironmentクラスの理由がおわかりいただけるでしょう。「外の環境をもちこんだラムダ式がクロージャ」という性質が、環境の
参照で実現されたわけです。
いっぽう元のLambdaは、あくまでも自分の環境しか参照先がありません。そのためラムダ式本体の評価時に自由変数があらわれた場合、
Closureだけは外の環境へ変数を探しにゆけます。


●クロージャを生成するタイミングとレキシカルスコープ規則

では、このような機能をもつClosureは、一体いつオブジェクトを生成すればいいのでしょうか? これは後述する"変数のスコープルール"を
どう考えるかによりますが、結論を先にいうと、先ほど解説したListクラスのfunction()メソッドにおいて

ラムダ式のfuction()が呼ばれた場合は、Lambdaオブジェクトを返す」「ラムダ式のfuction()が呼ばれた場合は、 Closureオブジェクトを返す


と変更することにします。つまり、ラムダ式から手続きオブジェクトを生成する場合は必ずClosureオブジェクトとするのです。

 これによって、トップレベル以下の、ラムダ式による文ブロックは、クロージャによって必ず外のブロックの環境を受け継ぐことになります。
そのため、あるブロック内の自由変数は、その変数が自分の外のブロック(最外でトップレベル)に存在すればクロージャの探索が届くことになり、
前回の例文の問題は解決します。<注> 

また同時に、たとえば同じ名前の変数が違うレベルのブロックに複数存在した場合、内側のブロックでの変数名への参照は必ず内側の変数を参照するため、
意図しない代入などもおきなくなります。<注>

<欄外注>このような束縛法をディープ(深い)バインディングといいます。
<欄外注>内側の変数が外側の変数をシャドウするといいます。

以上のような状況を「言語における変数のスコープルール」という側面からながめれば、全ての変数が構文ブロック(ラムダ式)を区切りにスコープが
決定される「レキシカルスコープ」に従ったことになります。またLET文やDO文のようなパラメータ変数の宣言を含むスペシャルフォームも、
やはりラムダ式と同じく変数の宣言を含むブロックなので、クロージャと同様の環境を実行時に持たせることで、全変数を完全にレキシカルスコープ規則で
統一しました。



■実行サンプル

以上で、ミニマムながらインタープリタの各機能が出そろいましたので、まずはさっそくサンプルを実行してみましょう。
もちろん再帰も可能です。以下はLISP原書第3版に掲載されている例題を動かしてみました。(図3〜5)。

図3 二重再帰によるフィボナッチ関数
--------------------------------------------

QLisp> (defun fibonacci (n)
QLisp> (if (or (= n 0) (= n 1))
QLisp> 1
QLisp> (+ (fibonacci (- n 1)) (fibonacci (- n 2)))))
FIBONACCI

QLisp> (fibonacci 0)
1
QLisp> (fibonacci 4)
5
QLisp> (fibonacci 20)
10946

--------------------------------------------
図4 一重再帰によるハノイの塔
--------------------------------------------

QLisp> (defun tower-of-hanoi (disks)
QLisp> (if (endp disks)
QLisp> 0
QLisp> (+ 1 (* 2 (tower-of-hanoi (rest disks))))))
TOWER-OF-HANOI

QLisp> (tower-of-hanoi nil)
0
QLisp> (tower-of-hanoi '(1))
1
QLisp> (tower-of-hanoi '(5 4 3 2 1))
31
QLisp> (tower-of-hanoi '(10 9 8 7 6 5 4 3 2 1))
1023
--------------------------------------------

図5 DOによる数のべき乗の計算
--------------------------------------------
QLisp> (defun do-expt (m n)
QLisp> (do ((result 1 (* m result))
QLisp> (exponent n (- exponent 1)))
QLisp> ((zerop exponent) result)))
DO-EXPT

QLisp> (do-expt 2 8)
256
QLisp> (do-expt 9 9)
387420489
QLisp> (do-expt 1 0)
1

--------------------------------------------

なおこの時点で全クラスをjarファイルにまとめてみるとファイルサイズは42Kバイトほどです。
JRE1.2と1.3rc1の両方を試し、1.2だとかなり遅いと感じたのですが1.3だとネイティブのLISP処理系と遜色ない速度で動作しました。 



■Quiltによるグラフィクス


さて第1回目でお話したグラフィクスの機能を追加しなければなりません。もともとこれをやりたくてLispの処理系を作成したと言っても過言ではなく、そういう意味ではクライマックスと言ってもいいところです。しかし、本稿がこの連載の最終回であり、他の部分の説明にも多くのページを費やさなければいけないので、グラフィクスの部分はほんの導入程度になってしまいました。 しかし、本連載の第1回のタイトルが示すとおり、私の中ではこのLispを使って対話的にキルト柄のような図形を表示するというのがこの連載の眼目だったので、とりあえずそこまでは説明したいと考えます。
 
今回作成した関数は次の5つです。

(1)(CREATEWINDOW width height)
 描画領域の大きさがwidth×heightのウィンドウを開く。戻り値はウィンドウIDを示す整数値

(2)CLOSEWINDOW winld)
 winldで示すウィンドウを閉じる

(3)(COLOR winld red green blue)

 指定のウィンドウのカレントカラーを設定する

(4)(LINE winld x1 y1 x2 y2)
 
直線を描画する

(5)(FILLPOLYGON winld(x1 y1 (x2 y2)....)
 
ポリゴンを塗りつぶします

これだけあればかなりのキルト柄を描くことが可能です。それではCREATEWINDOW関数の説明をします(リスト11)。関数の追加については、説明を省略します。手続きのブロックを見るとわかりますがFrameウィンドウとCanvasオブジェクトを作成しています。Canvasの大きさをwidth×heightにしています。また、(1)でウィンドウオブジェクト,(2)でグラフィクスオブジュエクトをリストに追加しています。そして(3)でウィンドウオブジェクトのリストのサイズを返しています。これはどういうことかというと、最初のウィンドウはwinList,graphicListのインデックス0に追加され、次のウィンドウはインデックス1に追加されますが、ウィンドウIDはそれに1を加えた番号にしました。たとえばウィンドウID=2番のウィンドウは2個目のウィンドウということになります。


リスト11 CreateWindow.java
--------------------------------------------------------------------

public class CreateWindow extends NormalFunction{
 //構文:(createwindow width height)
 //戻り値:ウィンドウを表す整数値(Integer)	
 //引数:width-ウィンドウの幅(Integer)
 //    height-ウィンドウの高さ(Integer)
static Vector winList=new Vector(); static Vector graphicList();=new Vector(); public CreateWindow() { }    public SExpression process(quilt.datetype.List araquments. Environment environment)throws Exception{   int argNum=araguments/size();  //文型チェック...引数は2つで整数
if(argNum<2)
throw new Exception("error-createwindow:too Few arguments");
if(argNum>2)   throw new Exception("error-createwindow:too many arguments");
if (!(arguments.get(0)instanceof quilt.datatype.Integer))   throw new Exception("error-createwindow:bad arguments type-"+ ((SExpression)arguments.get(0)).getStr()); if (!(arguments.get(1)instanceof quilt.datatype.Integer))
throw new Exception("error-createwindow:bad arguments type-"+ (SExpression)arguments.get(1)).getStr());

//手続き int width=((quilt.datatype.Integer)arguments.get(0))valueof(); int height=((quilt.datatype Interger)arguments.get(1)).valueof(); Frame win=new Frame(); winList.add(win);-----------(1) Canvas canvas=new Canvas(); canvas.setSize(width,height); win.add.(canvas); win.pack(); win.show(); win.toFront();
Graphics g=canvas.getGraphics(); graphicList,add(g);-----------(2) win.setTitle((new java.lang.Integer(winList.size())).toString());
return(SExpression)new quilt.datatype.Integer(wiList.size());-----------(3)  }
--------------------------------------------------------------------
リスト12 Line.java

//手続き
int i=((quilt.datatype.Integer)arguments.get(0))valueof();
int x1=((quilt.datatype. Interger)arguments.get(1)).valueof();
int y1=((quilt.datatype. Interger)arguments.get(2)).valueof();
int x2=((quilt.datatype. Interger)arguments.get(3)).valueof();
int y2=((quilt.datatype. Interger)arguments.get(4)).valueof();

Graphics g=(Graphics)Createwindow.grahicList.get(i-1);-----------(4) try{   g.drawLine(x1,y1,x2,y2);
catch(Excetion e){
throw e;){ } return (SExpression)new quilt.datatype.Integer(0);

--------------------------------------------------------------------
図6 CreateWindowの実行
--------------------------------------------------------------------
QLisp> (createwindow 300 300)
1
QLisp> (fiLLpoLgon 1 '(0 0) '(0 300)  '(300 0)
0
QLisp>(color 1 200 100 0)
0
QLisp>(fiLLpoLygon 1 '(300 300) '(0 300) '(300 0)
0
-------------------------------------------------------------------- 図7 図6の実行結果
 
--------------------------------------------

リスト13 sample.lisp
-------------------------------------------- (defun block (offx offy)
(color 1 200 100 0)
(fillpoLygon 1(List(+offx 0)(+offx 0)
(List (+offx 100)(+offy 0)(List(+offx 0)(+offy 100)))
(collor 1 100 200 100)
(fillpoLygon 1(List(+offx 100)(+offy 100)
(List (+offx 100)(+offy 0)(List(+offx 0)(+offy 100)))
(color 1 0 100 100)
(fillpoLygon 1(List(+offx 25)(+offx 25)
(List (+offx 25)(+offy 75)(List(+offx 75)(+offy 75)) (List (+offx 75)(+offy 25))) (defun quilt()
(createwindow 500 500)
(do(i 0(+i 100)))
((> i 500)
(do ((j 0(+j 100)))
((> i 500)
(bLock j i))))

--------------------------------------
図8 リスト13の実行結果


--------------------------------------------

次にLINE関数の手続き部分を見て見ましょう(リスト12)。(4)でCREATEWINDOWにあるスタティック変数graphicListkから、ウィンドウIDに対応するグラフィックスオブジェクトを取り出しています。そしてそのグラフィックスにに対してラインを描画しています。それではサンプルを2つほど、図6のように入力すると、ウィンドウに黒と褐色で三角形に塗り分けられます図7)。
 次は今回の最終サンプルのソースです。出力結果は図8
です。 このソースをエディタで編集しsample.lsp(リスト13 という名前で保存し以下のように実行します。

QLisp>(Load"sample.Lsp")
QUILT
QLisp>(quilt)

キルト模様が表示されたでしょうか。

        ◇ ◇ ◇
私がなぜキルト柄にこだわるかというと、趣味がお裁縫だからというわけではありません。何かコンピュータを使って絵を描こうとするときに大きく分けるとペイント系とドロー系ソフトを使うことになります。前者はラスタグラフィクス,フォトレタッチソフトでありその代表がPhotoshopやPainterなどで、ドット単位の編集をします。後者はベクターグラフィックスであり、代表としてはIllustrator やCorelDrawがあります。
しかし、いずれのソフトも手でポインティングすることには変わりなく、そういう意味ではアナログな編集ということができます。私はこれとは別にスクリプトによるグラフィクスの作成という可能性ももう1つの手法として重要だと考えています。平たく言えばプログラムで作成するグラフィックスという意味です。本誌の素晴らしい連載記事「Javaでグラフィックス」もそれにあたります。もちろんそれはVBやC++で描いてもいいのですが、コンパイラによるグラフィックスだとどうしても対話性に欠け試行錯誤し辛いので、それはお絵かきというより「開発」といった言葉がふさわしいように感じます。だから私は「なぜJavaには対話的な環境がないのか?」と不思議でしょうがないのです。また、LispにあこがれているからLispでそれを作ったのです。
 上記のようなシンプルなグラフィックス関数なら、小学生の高学年や中学生なら何の苦もなく使いこなせると思います。他のプログラム言語を知っている大人のプログラマには、プログラムで絵を描くことは頭で知っていることなので、さして興味を持ってもらえないかもしれませんが、頭の柔らかい子供の場合完全にはまってしまうかもしれません。
 Javaで描いておけばどんなPC上でも動作しますし、きっとそのうちプレステ2や当然MSのX−BOXでも動作するでしょう。現在Quiltのjarサイズは50Kバイト弱程度なのでモバイル端末でも動作しそうです。

 

■最後に

とりあえずグラフィックスのサンプルを表示して一応の目的は達成したと考えます。客観的に見て、当初考えていたJavaでLispを 作るメリットは完全に正しかったと確信しています。確か次のようなメリットを挙げていたと思います。

・ガーベッジコレクションをしなくていい
・日本語に対応する特殊処理がいらない
・大域脱出処理が例外処理を使えば簡単

・Listクラスを使えばコンスセルの複雑な処理をしなくてすむ
・オブジェクト指向で美しいソース
 

そしてこれら以外にも、中山さんが書いているように、JDK1.3になるとネイティブなLispと同程度のパフォーマンスを示すことがあるというのには、正直いって驚きました。それが本当ならこれは実用的なスピードであるということです。
 この連載も今回が最終回、本当はもう少し本格的名サンプルやよりアーティスティクスなグラフィックスのデモンストレーションで締めたかったのですが、『JAVA PRESS』のページ争いも熾烈を極めているようで、原稿の遅い私はいつもページを詰められてしまい(笑)、十分なサンプルを説明する紙面が足りなくなってしまいました。というのは嘘で本当は間に合わなかったのですが、まあ、このような堅めの記事を何十ページも読む人もいないと思いますが、機会があぅたら編集者に頼み込んで、このグラフィックスの発展した応用例を単発で書かせていただきたいと考えています。なお、この記事を書いた後、ホームページも立ち上げる予定です(http://www.laplata.ne.jp/javalisp/)。
 私の目論見は前の節で述べました。インタラクティブなグラフィックス環境が欲しいということ、それと、実はもう1つあります。21世紀のソフトウェアの最大の課題はおそらく再びAIということになると思います。知能とはいかないまでも、インテリジェントなアシスタント機能などが競って開発されることになるでしょう。きっと私も近い将来、そのときのいわゆるエキスパートシステム的なもの、プロダクションルールシステムを作る場合のベースに、このQuiltがなればいいなと思っています。また、今回の連載で共著した中山さんは2Dのアニメーションシステムの興味をもっており、Java版だけでなくC++版のLispを用いてアニメーションをスクリプト制御する野望を抱いています。この連載も7割ぐらいは彼の力によるものでした。何とか最後まで開発できたのは彼がいたからに他なりません。

 

前にもどる