Mio Lisp Document Release 0.1h (1999/1/5)



2004.7.11

全然更新してないのに、何かのご縁でせっかく訪れてくださったあなたにオマケ(・∀・)

下の内容からまったく進歩も進捗もないですが、なんだか Visual J# でもコンパイル通りました。
JAVA(TM) 信者のあなた、.NET 厨のキミ、Lisper のえらい人、石をなげないで....

とりあえず、VisualStudio2003 のプロジェクトフォルダまるごとここ↓においときますね。
コンパイル済みバイナリ(アセンブリ?)も含まれております(bin フォルダの下)。

miolisp.net20040711.zip

とりあえず、gui.lsp ,gui2.lsp, combinator.lspは動いてるみたいです。それしか確認してないです。

VisualStudio をインストールしていない人が実行してみるには、以下のランタイムが
必要になります。

「Microsoft Visual J# .NET Version 1.1 再頒布可能パッケージ」

また、.NET Framework 自体のインストールも当然必要です。

「Microsoft .NET Framework Version 1.1 再頒布可能パッケージ」

なお、上記の MS が提供するパッケージをインストールするにあたっては、それぞれの説明をよくお読みください。上記パッケージをインストールした結果、あなたの PC の Windows の挙動が変になっても、
私は責任を負えません。あしからず。


Mio Lisp とは

Mio Lisp はにしやまの現実逃避 行動の一環として作成された、Java(JDK 1.1.* 相当)で記述された Lisp 風 言語処理系です。

はじめにお断りしておきますが、開発途上ということもあり、 きちんとしたモノには程遠いです。 なにかに流用、もしくはそのまま Lisp 処理系作成のレポートとして提出しよう などという学生の方はソースを見てがっかりしないようにしてくださいね(笑)

Mio Lisp は GNU GPL に基づいた配布ポリシーのフリーソフトウェアです。 配布条件に付いては、パッケージに附属のライセンス 文書 (和文)を御覧下さい。

このテキストを含むMio Lisp の最新版は こちら(jar archive, jar,unzip で解凍可)から入手できます。
動作確認環境は、インストールガイドを御覧下さい。

「開発を手伝ってもいい」という奇特な方、もしおられましたら御連絡おまちしております。
 

特徴

これといった特徴はありませんが、強いて挙げると…

現在の開発状況

最近やったこと

暇があったらやりたいこと

現在の仕様

なにぶん現実逃避なので、どんどん変わる可能性があります。

環境

データ型

データ型 ソースファイル中の
対応するクラス名
ソースファイル中の
親クラス名
備考(実装に関する情報)
シンボルアトム SAtom Atom この型のアトムにのみ、値を束縛可能。 グローバル値および属性リスト(未実装)は、 このクラスのインスタンス内に保持される。 シンボルの唯一性を保持するために、全インスタンスへの参照を 保持するハッシュテーブルをクラス変数に持つ。 なお、この Lisp においてシンボルは全て大文字、あるいは大文字小文字の 混合で表記された場合でも、全て小文字として取り扱われる。
数値 (Number) --- java.lang.Number 以下のクラスで流用する仕様に変更。 評価すると、値そのものを返す。
注意:リーダ (Sreader.java)は Double しか読めない。数値を読むと、java.lang.Double クラスのオブジェクトになる。(1/5)
文字列 (String) --- Java.lang.String を流用する仕様に変更。評価すると、値そのものを返す。
コンスセル Cons (Object) --
マクロ関数 Macro (Object) S 式で記述された関数(マクロ)。引数は評価しない
ラムダ関数 Lambda Macro S 式で記述された関数。引数を評価
Java ネイティブ関数 NativeFunction (Object) このクラスのインスタンスは、java.lang.reflect.Method のオブジェクト (実体は LispFunction インターフェイスを継承するクラスの static メソッド) を内部に保持する。
MioLisp の内部定義関数、拡張関数はすべてこのタイプ
Java メソッド
Java コンストラクタ
(Method)
(Constructor)
--- リストの car 部に Method,Constructor があるリストを評価すると、 cadr 部のオブジェクトに対し、invoke を使って残りのリストを 引数として Java メソッドを call する
Java メソッド配列 JavaMethod (Object) リストの car 部にメソッド配列があるリストを評価すると、 cadr 部のオブジェクトに対し、invoke を使って、 残りのリストを引数として、配列の中から引数の形の 合致する Java メソッドを call する
(import) の拡張に伴って導入されたデータ型。 詳細は動的呼び出しのマップ を参照のこと。
(Java オブジェクト) (上記以外の Object) --- Cons の car,cdr 部、およびシンボル束縛は 上記以外の Java オブジェクトへの参照も保持できる

 

リーダ

(該当クラス:Sreader)
java.io.StreamTokenizer をほとんどそのまま使っています。 数値は現在すべて Double に変換されます。
読み込んだ場合に特殊処理をするキャラクタを以下に示します。

キャラクタ 読み込み時の処理、変換など
'(クオート) 'hogehoge を (quote hogehoge) に変換
`(バッククオート) `hogehoge を (backquote hogehoge) に変換
,(コンマ) ,hogehoge を (comma hogehoge) に変換
(backquote 内で、マクロ展開に使用)
,@(コンマ+アット) ,@hogehoge を (atmark hogehoge) に変換
(backquote 内で、マクロ展開に使用)
.(ピリオド) リスト中に現れた場合、直後の要素への参照をそのセルの cdr 部とする。
(a . (b c)) は (a b c) と等価
"(ダブルクオート) ダブルクオートで区切られた部分を文字列型(java.lang.String)として処理
;(セミコロン) 行末(EOL)までをコメントとみなす

 

評価手順

(該当クラス:Eval)
  1. Atom の評価
    Atom クラスに実装されている Object eval(Environment e) メソッドが呼ばれ、 ローカル環境(e)、グローバル辞書の順で値を探す。

  2. 文字列(java.lang.String) の評価
    その値(String)を返す

  3. 数値(java.lang.Number) の評価
    その値(Number)を返す

  4. List(Cons) の評価

    1. car 部が SAtom のとき、
      car 部を 1. と同様に評価した結果に car 部を読み換え、C. 以下へ
      例:(cons 1 2) -> (#<NativeFunction ...> 1 2)
       
    2. car 部が Cons のとき、
      car 部を 4. と同様に(再帰的に)評価した結果に car 部を読み換え、C. 以下へ
      例:((lambda (x) x) 2) -> (#<Lambda ...> 2)
       
    3. car 部が Macro もしくは Lambda (= S 式で定義された関数もしくはマクロの場合)のとき Macro.eval(Object cd,Environment e)を call した結果を返す。
       
    4. car 部が NativeFunction のとき (= Java で書かれ、import された関数の場合) Method オブジェクトを取り出し、第一引数を null,第二引数にする配列に 環境 (Object o[0] = e)、cdr 部を代入(o[1] = cd)して、 java.lang.reflect.Method.invoke() を call した結果を返す

      *この場合、呼ばれる側は常に static Object hogehoge(Environment e,Object o) となる。
       

    5. car 部が Java メソッドオブジェクト(java.lang.reflect.Method) のとき、 (= native で生成された Java Method) cadr を評価した結果を第一引数に、 cddr 以降を順次評価した結果をオブジェクトの配列に展開したものを第二引数に、 java.lang.reflect.Method.invoke() を call した結果を返す

      *static メソッドの場合は第一引数は null を渡す
       

    6. car 部が Java コンストラクタオブジェクト(java.lang.reflect.Constructor) のとき、(= native で生成された Java Constructor) cdr 以降を順次評価した結果をオブジェクトの配列に展開したものを引数に、 java.lang.reflect.Constructor.newInstance() を call した結果を返す
       
    7. car 部が JavaMethod のとき、 JavaMethod 内部に保持している Method の配列の中から、 引数の型の合致するメソッドを探し、cadr を評価した結果を第一引数に、 cddr 以降を順次評価した結果をオブジェクトの配列に展開したものを第二引数に、 java.lang.reflect.Method.invoke() を call した結果を返す

      *static メソッドの場合は第一引数は null を渡す
       

    8. car 部がその他のオブジェクトで、かつ cadr 部が文字列の時、 car 部の(フル)クラス名を文字列にしたものに cadr 部の文字列を連結して シンボルを作成、そのシンボルの大域値を参照してその値が JavaMethod で ある場合、JavaMethod 内部に保持している Method の配列の中から、 引数の型の合致するメソッドを探し、cadr を評価した結果を第一引数に、 cddr 以降を順次評価した結果をオブジェクトの配列に展開したものを第二引数に、 java.lang.reflect.Method.invoke() を call した結果を返す
       
    9. 以上のいずれでもないとき、エラー
       
  5. 以上のいずれでもないとき、エラー
     
この Lisp では関数辞書を特別に持たないため、 基本関数も関数実体(オブジェクト)は、その名前のシンボルに束縛されており、 eq,car,cdr 等の基本関数、lambda オブジェクトの生成、 backquote の展開等は全て上記 D. の仕組みを使って実現しています (基本関数も起動時に動的に読み込んでいます)。
 

基本関数(Java で定義された関数)

前項でも述べた通り、基本関数(実際には関数への参照)は全て起動時にロードされます。 現在のトップレベルプログラム (関連クラス:MyLisp) では、Primitive という名前のクラスが自動的に 読み込まれ、そこで定義されている関数が基本関数として読み込まれるしくみに なっています。現在定義されているのは以下の通りです。 (実際には関数でないものも含まれていますが、実装はこの後で述べる関数と 同様のインターフェイスでなされています)

基本関数の拡張は、上記 java ソースファイルに以下のシグネチャを持つ メソッドを追加することで行えます。

<FUNCNAME> は読み込まれた時点で全て小文字に変換されます (SAtom の規則による)のて、null など、java の予約語とぶつかるような 関数名を定義したい場合は、適当に大文字を混ぜるなどしてください。

第一引数の e はローカル環境です。環境に対する操作を伴う関数を実装する場合、 直接 Environment クラスのメソッドを使用する必要があるかもしれませんが、 大抵は Eval.eval() の引数に使います。

第二引数の o は評価時点での cdr 部(評価の項 3-D を参照)の 先頭のセルへの参照がそのままの形(評価される前の形)で入っています。 通常の関数であれば、まず引数リストを評価する必要があります。

評価、およびリストの要素を順に取り出す操作などを簡単にするためのクラスとして、 EvalutilListIter が用意されています。 使い方は、えっと、Primitive.javaを参考にしてください(笑)

但し、ListIter を使うと、引数の数を最後の引数を評価する段階で チェックすることになるので、評価の最後で引数の個数が原因でエラーが 発生した場合、環境に影響がのこる可能性があります。 Macroクラス等の S 式で定義された関数の評価では、引数の数のチェックを評価前に行っているので、 不整合がありますが、今のところ気にしないことにしています。 だれかもっと高速でエレガントな仕組みを作って僕に下さい(笑)。
 

関数定義(S 式で定義された関数)について

だいぶ力尽きたので、例をいくつかあげて逃げます。 辞書を共有している関係上、というか、手抜きのため、この Lisp では 関数定義は Scheme に似た以下のような方法で行います。 ここで define はグローバル辞書に値を束縛する基本関数です。

第二引数の (lambda ...) が評価された時点で、 この部分は Lambda クラスのオブジェクトになります。

このように定義した関数呼出は

で行えます。 (シンボルアトム myfunc に Lambda クラスのオブジェクトが束縛されている状態) リストの car 部が常に評価され、それが関数であれば関数として (なんのこっちゃ)呼ばれるという方法なので、このことを利用して たとえばいわゆる mapcar 関数が以下のように書けます。

Macro も Lambda と同様に関数オブジェクトですが、 引数が評価されない、定義式が2度(1回めはローカル引数リストに未評価の 引数が束縛された状態で評価し、2回目は元の環境で1回目の評価結果を)評価 される点が異なります。

また Lambda は (lambda ()) 評価時点(Lambda オブジェクト生成時点)のローカル 環境のコピーをオブジェクト内に保持しており、関数本体の評価時に それをひきずりますが、(closure とかいうものらしい(笑))が、 Macro はこれを行ないません。

え、なぜそうなってるか、ですか。えっとですね、実は僕も良く分かりません。 なんとなくそっちのほうが辻褄が合いそうな気がするからです。 辻褄が合わないことが分かれば、変えるかも知れません。

マクロ定義を見やすくするため、バッククオートとカンマ表記を サポートしています。これらの表記はリーダで S 式表現に展開され、実際の展開はPrimitive 内の backquote() (拡張関数形式) で行っています。

以下は Common Lisp 風の defun,defmacro を実現する Macro の例です。 参考にしてください。なんとなく分かると思います。

なお、この例に登場する & で始まるアトムですが、現在 2 種類あり、 &rest は引数の残りをまとめてつぎの変数に代入、 &aux に続く引数はローカル変数になります。 Lambda,Macro 両方の引数部で使えます。

ありがちな(なんだそりゃ) &optional や &key は未実装です。
 

エラー

リーダ実行中のエラーは(SreaderException) 評価時のエラーは(EvalException)を throw する様になっていますが、 現在のトップレベルではそれを catch して単にスタックトレース(Java VM の)を 表示しているだけです。

一応メッセージ部にそれらしい要因を入れていますが、 問題が起こった時点行き当たりばったりで throw しているため、 フォーマットも含め統一されているとは言いがたい状況です。

本来エラーの種類別に細分化するのが筋かと思いますが、 面倒そうなので手をつけていません。
 

ガベージコレクション

なんにも考えていません。用済みの辞書やコンスセルは JavaVM が始末してくれる筈です(多分)。

ただ、一旦登録されてしまったシンボル(SAtom)は消えないので増える一方です。 さて、どうしましょうか ;-)
 

JDK 等 Java Class の呼出方法について

Java で記述されたクラスをより容易に利用するための仕組みとして、 import (実装は GetNative)、評価部を拡張しました。

おおまかには、(import "classname") で JDK 等一般のクラスも ロードできるようにし、それらのメソッドはフルクラス名とメソッド名を ピリオドで接続した印字名をもつシンボル(SAtom) に 大域値として束縛されます。

同じ名前で引数のパターン(シグネチャ)が違うメソッドを取り扱えるようにするため、 Java で記述された基本関数をロードする際に 行っていたような、 Method そのものを関数名のシンボルに束縛する方法ではなく、 新たに JavaMethod クラスを 作成し、その中に同名の Method を複数保持できるよう、Method の配列の形で 保持しています。

たとえば、(import "java.lang.String") を行うと、 java.lang.String クラスの toUpper メソッドは、 JavaMethod オブジェクトの形で java.lang.string.toupper という名前をもつ シンボルに束縛されます。

これで使用するメソッドごとに (native) を用いて生成する必要はなくなるものの、 呼び出し部の記述は同様に面倒です。

そこで、評価部を拡張し、

のような記述で import されたメソッドを呼び出せるよう、 car 部が Macro,Lambda,NativeFunction,Method,Constructor オブジェクトで 無い場合で、かつ cadr 部が文字列のとき、 上記の記述を と読み変えて評価することにしました。 これで、例えば文字列を大文字にする場合、(見ためすこし変ですが) のように記述できます。

この方法を用いた附属のサンプル(gui2.lsp)も 御覧下さい。

1999/1/5 以前のバージョンでは、メソッドの取得に Class.getDeclaredMethods() を 使っていたため、メソッドを Object まで遡って取得するというまぬけなことを してましたが、Class.getMethods() を使う事に変更したため、この部分に関しての 記述をドキュメントから削除しました。(1999/1/5)

また、改めて見直すと、使わないものも含めて、全てのメソッドへの参照を 保持する必要性がないような気がします。そこで、

に出会った時にはじめてメソッドを検索する方式に変更し、Java Class の 呼出手順としての (import "....") を廃止する事を検討中です。(1999/1/5)
 

参考文献

Mio Lisp を作成するに当たっては以下の文献を参考にしました。

書籍名 著者/出版元 備考
初めての人のための LISP 竹内郁雄著
サイエンス社
ISBN4-7819-0454-8
マクロ、Closure のしくみなど、実装にあたって欲しい情報の ほとんどはこの本に書いてありました。 雑誌の連載をまとめた物のようで、対話形式になっており、 リファレンスとして使うには難がありますが、 買って損はないイチ押しの本です。 結構昔に買った本ですので、 まだ売ってるかどうかは分かりません(笑)
JAVA IN A NUTSHELL, 2nd Edition David Flanagan著
O'Reilly & Associates, Inc.
ISBN1-56592-262-X
Sun から日本語版の HTML マニュアルが提供されているとはいえ、 JDK のメソッド定義の逆引き、定義クラス一覧など、Java でちょっとした おもちゃを作る際には手放せない 1 冊。JDK 1.0 対応の前版はもうすこし 小振りの大きさで持ち歩くのによかったのですが、肥大化しました。 (それとも、小型のも出てるんでしょうか?)

Mio Lisp に関しての感想、御意見は、にしやま mio@mio.rim.or.jpまで。