現在Javaワールドで最もエキサイティングなことの1つは、別のプログラミング言語を仮想マシンで動くようにすることです。JRuby、Groovy、Scala、およびJavaScriptエンジンRhinoの周囲にはさまざまな試みがあります。しかし、どうしてそこで止めてしまうのでしょう。メインストリームの外側へ一歩を踏み出したいと本当に思うなら、Javaとは完全に異なる世界に飛び込もうと考えるなら、Lispは重要なオプションです。しかも、オープンソースのJVM用Lispプログラミング言語実装は複数あり、すぐに検討を始められます。
ところで、Lispの価値は何だと思いますか。1つには、この50歳の言語が、我々が今日あたりまえのことと考える多くのアイディアの触媒となってきたことです。if-then-else構造はもともとLispから発生しましたし、オブジェクト指向やガベージコレクションを備えた自動メモリ管理の初期の試みも同様です。現在のJavaプログラミングのホットトピックであるレキシカルクロ-ジャ(Lexical closure)は、最初に、1970年代Lispにおいて研究されました。またそれ以外にもLispには他の言語がまだ採用していない固有の機能と将来復活するだろう優れた種類の概念が数多くあります。
この記事は、Lispに興味のあるJava開発者を対象にしています。現在JVM上で利用できる、Lispの異なる方言について検討し、Lispのプログラミング方法とLispプログラミングに固有の問題を速習できるようになっています。最後に、LispコードをJavaシステムに組み込む方法について述べます。
複数のプラットフォームで利用可能なLispは、フリーのものと市販のものの両方が数多く存在します。Lispの検討を始めようとするJavaユーザーの場合、JVMにとどまることが最良の第1選択です。開始が容易ですし、慣れ親しんだ既存のJavaライブラリとツールをすべて利用できるメリットがあります。
COMMON LISPとSCHEME
Lispには、Common LispとSchemeの2つの主な方言があります。この2つは多くの同じ概念に基づいていますが、どちらがより優れた選択肢であるかについて激しい議論が持ちあがるほどの違いがあります。
Common Lispは、1991年に確定されたANSI規格で、それより前の複数のLispの概念を統合しています。多くの種類のアプリケーション(中でも最もよく知られているものは人工知能)の開発のために設計された大規模な環境です。一方、Schemeは学問の世界から生まれ、より慎重に最小化されており、コンピュータ科学の教育にも組み込みスクリプトにも適した言語であると判明しています。これまでに出会った可能性のある、この他の良く知られたLispは、Emacs LispやAutoCADのAutoLISPのような小さいアプリケーション専用のDSLです。
JVM上に両方の主な方言の実装がありますが、Schemeがより完成されています。Armed Bear Common Lisp (www.armedbear.org/abcl.html)は、合理的に完成されたCommon Lisp規格の実装ですが、別のCommon Lispシステムがインストールされていないかぎり、ディストリビューションをビルドできないという問題があり、初心者には悩みの種になるかもしれません。
Scheme陣営には、2つの主なプレーヤー、Kawa (www.gnu.org/software/kawaとSISC (www.sisc-scheme.org-- Schemeコードの第2のインタプリタ)があります。この記事のサンプルではKawaを使用します。Kawaは、実際にはJavaバイトコードにコンパイルされた新しい言語を作成するフレームワークであり、Schemeはその実装の1つです。付け加えると、その製作者であるPer Bothner氏は、現在Sun社のJavaFXのコンパイラのプロジェクトに従事しています。
その他の注目に値する競争相手には、Clojure (clojure.sourceforge.net)(リンク)があります。これは新しい言語で、Lispの方言を含み、SchemeとCommon Lispの中間に位置します。直接JVMを対象に設計されたため、これまで述べたすべてのLispの中で最もすっきりしたJavaインテグレーションを持ち、また、同時実行トランザクションメモリのビルトインサポートなど、その他のおもしろい複数の概念を特長としています。現在、Clojureはまだ試験的なベータ段階ですので、その上で何かをビルドするには少し早すぎるかもしれませんが、今後目を離せないプロジェクトであることは明らかです。
THE READ-EVAL-PRINT-LOOP
Kawaのインストールから始めましょう。ディストリビューションは1個のjarファイルで、ftp://ftp.gnu.org/pub/gnu/kawa/kawa-1.9.1.jarから直接ダウンロードできます。jarを入手したら、classpathに追加します。以下のコマンドを実行することで、REPLの開始の準備ができます。
java kawa.repl #|kawa:1|#
これで、Kawaが起動され、プロンプトが表示されます。ところでREPLとはなんでしょうか。REPLとはREAD-EVAL-PRINT-LOOP(読み込み-評価-プリント-ループ)を意味し、実行中のLispシステムと対話する1つの方法です。REPLは、入力を読み込み(READ)、評価し(EVALuate)、結果をプリントし(PRINT)、もう一度プロンプトに戻ります(loop)。Lispプログラムの開発方法は、Javaのプログラミング時に実施される「コードを書き、コンパイルし、実行する」サイクルとは、多くの場合異なります。Lispプログラマは一般に、Lispシステムを起動し実行したままにして、コンパイルとランタイムの間の境界をあいまいにします。REPL内では、関数と変数を実行中に変更できます。コードは動的に解釈(interpret)されるか、または、コンパイルされます。
はじめに、とてもシンプルなものを示します。次の例は2つの数を加算します。
#|kawa:1|# (+ 1 2) 3
これは典型的なLispの式の構造、「フォーム(form)」です。構文は非常に合理的であり、式は常に丸括弧で囲まれ、プレフィックス記号を使用します。このため、2つの項の前にプラス(+)があります。より高度な構文にするには、複数のフォームをネストして、ツリー構造を作ります。
#|kawa:2|# (* (+ 1 2) (- 3 4)) -3
Schemeの組み込み関数も同様に機能します。
#|kawa:3|# (if (> (string-length "Hello world") 5) (display "Longer than 5 characters")) Longer than 5 characters
ここでは、特定の文字列の長さが5文字以上かどうか確認するif文があります。この例のように真(true)であれば、次の式が実行され、メッセージがプリントされます。なお、ここでのインデントは単に読みやすくするためであることに注意してください。このプログラム全体を1行に書こうと思えば、それも可能です。
Lispコードで使用される、丸括弧を重ねたスタイルは、「S式(s-expression)」と呼ばれています。これは、XMLに非常によく似た、一般的な構造データの定義方法も兼ねています。Lispには、多くの組み込み関数があり、これを使うことでS式形式でのデータ処理がとても容易になります。これは言い換えればLispの強みでもあります。構文が非常にシンプルであるため、プログラムの記述(コードの作成または修正)が、他の言語よりもずっと簡単になります。後でマクロの例を見ると、このことがさらによく分かります。
関数
Schemeは通常、関数型プログラミング言語のファミリーの一員と考えられています。オブジェクト指向の世界とは異なり、抽象の主な方法はクラスとオブジェクトではなく、関数とそれが取り扱うデータです。Schemeで実行できることはすべて、本当に、複数の引数を受け取り、結果を返す関数を呼び出すことです。関数を作成するには、define キーワードを使用します。
#|kawa:4|# (define (add a b) (+ a b))
これは、2つの引数、aとbを受け取るaddを定義しています。関数の本体は単純にプラス(+)を実行し、自動的に結果を返します。static型宣言がないことに注意してください。すべての型チェックは他のダイナミック言語と同様に実行時に行われます。
関数を定義すると、次のようにREPLから単純に呼び出すことができます。
#|kawa:5|# (add 1 2) 3
関数はScheme世界の第1級市民であり、Javaのオブジェクトのように順に引き渡すことができます。このことから非常に興味深い可能性が開かれます。1つの引数を受け取り、それを2倍する関数の作成から始めてみましょう。
#|kawa:6|# (define (double a) (* a 2))
次にlist関数を呼び出して、3個の数字のリストを定義します。
#|kawa:7|# (define numbers (list 1 2 3))
さあ、ここからがおもしろいところです。
#|kawa:8|# (map double numbers) (2 4 6)
ここでは、mapを呼び出しています。mapは、引数として別の関数と何かのリストの2個を受け取ります。mapは、リストの各要素に対して繰り返して与えられた関数を呼び出します。結果は新しいリストに集められ、REPLに返され、ユーザーが見ることができます。これは、Javaではforループを使って解決されることを、より「関数的に」行う方法です。
LAMBDAS
もっと便利な方法はlambdaキーワードを使用して匿名関数を定義することです。これは、Javaで匿名内部クラスを動作させる方法に似ています。上のサンプルを修正し、中ほどのdouble関数定義をスキップして次のようにmap文を書くことができます。
#|kawa:9|# (map (lambda (a) (* 2 a)) numbers) (2 4 6)
単にlambdaを返す関数を定義することもできます。古典的な教科書のサンプルは、次のようなものです。
#|kawa:10|# (define (make-adder a) (lambda (b) (+ a b))) #|kawa:11|# (make-adder 2) #
ここで何が起こったのでしょうか。最初に、引数1つを受け取るmake-adder という名前の関数を定義しました。make-adder は、別の引数、「b」を期待する匿名関数を返します。呼び出されたとき、匿名関数は実行時にa とbの合計を計算します。
(make-adder 2)、すなわち「渡した引数に2を加える関数を返してください」を実行すると、文字列としてプリントされた実際のlambdaプロシージャである暗号のようなものをREPLがレポートします。make-adderを使用するには以下のように実行します。
#|kawa:12|# (define add-3 (make-adder 3)) #|kawa:13|# (add-3 2) 5
ここでの重要なポイントは、lambdaがクロージャのように動作することです。「全体を囲み」、作成されたときのスコープ内にある変数への参照を維持します。(make-adder 3)呼び出しの結果であるlambdaがaの値を保持しているため、(add-3 2)が実行されたときに3 + 2が算出でき、期待どおりに5を返すことができます。
マクロ
これまで見てきた機能は、より新しいダイナミック言語に見られるものと非常によく似ています。例えばRubyでは、ちょうどこの前にlambdaと map
関数を使用して行ったこととまったく同じように、匿名ブロックを使用してオブジェクトのコレクションを処理できます。ここでは頭を切り替えて、他にないLispy(Lisp的なもの)、つまりマクロを見てみましょう。
SchemeにもCommon Lispにもマクロシステムがあります。Lispを「プログラム可能なプログラミング言語」と呼ぶ人がいるのは、このマクロのためです。マクロを用いると実際にコンパイラをフックして、言語そのものを再定義できます。このことから、Lispの一貫した構文が大きく効果を上げ、すべてが興味深くなります。
簡単な例として、ループをみてみましょう。元々、Scheme言語には、定義されたループはありません。何かを繰り返す従来の方法は、mapまたは再帰関数呼び出しのいずれかを使用することでした。テールコール(tail-call)最適化と呼ばれるコンパイラ技術のおかげで、スタックをあふれさせることなく再帰を利用できます。後に、非常に柔軟なdoコマンドが導入されました。このコマンドを使用してループを実行するには、以下のようにします。
(do ((i 0 (+ i 1))) ((= i 5) #t) (display "Print this "))
ここでは、インデックス変数iを定義し、ゼロで初期化して、繰り返しごとに1ずつ増加するようにします。式(= i 5)が真と評価されたときにループが終わり、続いて#t (SchemeにおいてJavaのブールの真と等価なもの)が返されます。ループの内部では、単に文字列をプリントします。
単に単純なループが必要なのであれば、これはかなり複雑な定型コードです。場合によっては、以下のようにもっと簡単に実行できるほうが望ましいでしょう。
(dotimes 5 (display "Print this"))
マクロのおかげで、define-syntax 関数を適切に使用して、特別構文dotimesを言語に加えることができます。
(define-syntax dotimes (syntax-rules () ((dotimes count command) ; Pattern to match (do ((i 0 (+ i 1))) ; What it expands to ((= i count) #t) command))))
これを実行すると、dotimesの呼び出しは特別な方法で取り扱う必要があることがシステムに通知されます。Schemeは、定義された構文規則を使用してパターンを照合し、結果をコンパイラに送る前に展開します。この例では、パターンは(dotimes count command)であり、通常のdoループに変換されます。
これをREPLで実行すると、次のようになります。
#|kawa:14|# (dotimes 5 (display "Print this ")) Print this Print this Print this Print this Print this #t
この例の後で、2つの疑問が浮かび上がってきます。第1に、いったいなぜマクロを使用する必要があるのでしょうか。同じようなことを通常の関数ではできないのでしょうか。答えは「できない」です。関数の呼び出しは、実際には、すべての引数が送られる前に引数の評価を起動し、この例ではうまく機能しません。例えば、(do-times 0 (format #t "Never print this"))をどう処理するのでしょうか。評価は後にする必要があります。したがって、マクロを使ってしか実現できないのです。
第2にマクロの内部で変数iを使用しています。もしもcommand式内でたまたま同じ名前の変数を使用したら衝突が発生しないでしょうか。心配いりません。Schemeのマクロは、「ハイジニック(hygienic)」と呼ばれるものです。コンパイラは自動的に名前付けの衝突を検出し、このようなときにどう対処すればよいかを分かっているので、プログラマにとって完全に透過的です。
これを見た後で、自分自身のループ構造をJavaに追加することを考えてみてください。不可能に近いでしょう。いえ、完全に無理というわけではありません。なんといってもコンパイラはオープンソースですから、ダウンロードして作業に入ることはできます。しかし実際には現実的なオプションではありません。他のダイナミック言語では、クロージャはもう少し進んでいて、言語を希望する形にすることができます。しかし、構文を完全に調整できるほどには構造が強力でない、あるいは、柔軟でないケースもあります。
このようなマクロの強力な機能が、話題がメタプログラミングやドメイン固有言語である場合にLispがしばしば勝ち残る理由となっています。長い間、(その逆ではなく)言語そのものを問題のドメインに合わせる場合には、Lispプログラマが「ボトムアッププログラミング」のチャンピオンでした。
JAVAからのSCHEMEコードの呼び出し
JVM上で別の言語を実行する主なメリットの1つは、コードに書かれた内容を既存のアプリケーションに統合できることです。頻繁に変更されることの多い複雑なビジネスロジックのモデル化にSchemeを使用し、より安定したJavaフレームワークに組み込むことは簡単に想像できます。ルールエンジンJess (www.jessrules.com)は、この方向に進んでいる構想の一例で、JVMで動作していますが、ルールの宣言には独自のLispに似た言語を使用します。
しかし、異なるプログラミング言語の相互運用性をすっきりした方法で機能させることは、特にJavaとLispのように2言語の間の不整合が大きい場合には、非常に難しい問題です。この統合を実施する方法に標準はなく、JVMで動作する方言ごとに異なる方法で問題に対処します。KawaはJavaの統合に対して比較的優れたサポートがあり、Schemeコードを用いたSwing GUIを定義する方法を、引き続きKawaを使って検討します。
JavaプログラムからKawaコードを起動することは簡単です。
import java.io.InputStream; import java.io.InputStreamReader; import kawa.standard.Scheme; public class SwingExample { public void run() { InputStream is = getClass().getResourceAsStream("/swing-app.scm"); InputStreamReader isr = new InputStreamReader(is); Scheme scm = new Scheme(); try { scm.eval(isr); } catch (Throwable schemeException) { schemeException.printStackTrace(); } } public static void main(String arg[]) { new SwingExample().run(); } }
この例では、swing-app.scm という名前のSchemeプログラムを含むファイルがclasspathのいずれかにあることが想定されています。インタプリタの新しいインスタンス、kawa.standard.Schemeが作成され、呼び出されてファイルの内容が評価されます。
Kawaは、Java 1.6に導入されたJSR-223スクリプティング(scripting) API (javax.scripting.ScriptEngine等)をまだサポートしていません。これをサポートするLispが必要であれば最善策はSISCです。
SCHEMEからのJAVAライブラリの呼び出し
より大きなLispプログラムを書き始める前に、作業にもっと適したエディタを探す必要があります。さもなければ、すべての丸括弧を常に監視し続けるために、気が変になるでしょう。最も一般的な選択肢の1つはもちろんEmacs(なんといってもそれ自体をLispの方言でプログラムできます)ですが、Java開発者の場合はEclipseを使い続けるほうが無理がないでしょう。その場合は、先に進む前にフリーのSchemeScriptプラグインをインストールする必要があります。schemeway.sourceforge.net/schemescript.html(リンク)で入手できます。また、Cuspと呼ばれるCommon Lisp開発用のプラグインもあります(bitfauna.com/projects/cusp)(リンク)。
ここでは、swing-app.scmの実際のコードを見ながら、Kawaを使用する簡単なGUIの定義に必要なことを検討します。以下の例は、中にボタンが1つある小さなフレームを開きます。ボタンを一度クリックすると、ボタンは無効になります。
(define-namespace JFrame) (define-namespace JButton ) (define-namespace ActionListener ) (define-namespace ActionEvent ) (define frame (make JFrame)) (define button (make JButton "Click only once")) (define action-listener (object (ActionListener) ((action-performed e :: ActionEvent) :: (*:set-enabled button #f)))) (*:set-default-close-operation frame (JFrame:.EXIT_ON_CLOSE)) (*:add-action-listener button action-listener) (*:add frame button) (*:pack frame) (*:set-visible frame #t)
最初の数行は、Javaのimport文に似ていますが、define-namespaceコマンドを使って、使用しようとしているJavaクラスの短い名前を設定しています。
次に、フレームとボタンを定義します。Javaオブジェクトはmake関数を使って作成されます。ボタンでは、コンストラクタに引数として文字列を渡します。Kawaはよくできていて、必要に応じて文字列をjava.lang.Stringオブジェクトに変換します。
ここでは、ActionListenerの定義をとばして、まず最後の5行を見てみましょう。ここでは、記号「*:」はオブジェクトのメソッドの起動に使われています。例えば、(*:add frame button)はframe.add(button)と等価です。また、メソッドの名前が自動的にJavaのキャメルスタイル(camel-case style)から、Schemeで一般的なダッシュで分割された小文字の単語になっていることにも注意してください。例えばset-default-close-operation は裏でsetDefaultCloseOperationに変わります。もう一つ詳細な点は、「:.」がどのように静的フィールド(static field)へのアクセスに使用されているかということです。(JFrame:.EXIT_ON_CLOSE)はJFrame.EXIT_ON_CLOSEと等価です。
では、ActionListenerに戻りましょう。ここでは、object関数を使用して、java.awt.event.ActionListener インタフェースを実装する匿名クラスを作成しています。action-performed関数にボタンのsetEnabled(false)呼び出しを設定しています。action-performedがActionListener インタフェースで定義されたvoid actionPerformed(ActionEvent e)の実装であると推定されることをコンパイラに知らせるために、なんらかの型の情報がここに追加される必要があります。上でSchemeでは一般的に型が必要ないと説明しましたが、Javaと対話するこのケースでは、コンパイラには追加の情報が必要です。
これら2つのファイルを用意したら、SwingExample.javaをコンパイルし、結果のクラスとswing-app.scm がclasspathのいずれかにあることを確認します。次に、java SwingExampleを実行しGUIを確認します。また、load関数、(load "swing-app.scm")を使用して、REPLにファイル内のコードを評価させます。これで動的にGUIコンポーネントを操作するドアが開きます。例えば、REPLのプロンプトで(*:set-text button "New text")を実行するとボタンのテキストを切り替えることができ、その変更がすぐに有効になるのを見ることができます。
もちろん、この例は単にKawaからJavaを呼び出す方法を示す意味しかありません。決して、これが想像できる最もエレガントなSchemeコードなのではありません。むしろ、Schemeで大きなSwing UIを定義しようとするなら、もっと上手に抽象レベルを上げ、賢明に選択されたいくつかの関数とマクロの陰に乱れた統合コードを隠すことができるでしょう。
参考情報
この記事を読むことでLispへの興味を刺激されたことを期待します。まだ調査すべきことがたくさん残っていることは間違いありません。さらなる学習に役立つ情報には以下のものがあります。
- Structure and Interpretation of Computer Programs -- この代表的なコンピュータ科学の教科書は、Schemeの優れたガイドです(mitpress.mit.edu/sicp)(リンク)。
- Practical Common Lisp -- Common Lispに関する最高で最新のチュートリアル (www.gigamonkeys.com/book)。
- Beating the Averages -- スタートアップ企業の隠れた武器としてのLispに関するPaul Graham氏の議論の的になっているエッセイ (www.paulgraham.com/avg.html)。
- The Nature of Lisp -- XMLとAntから始め、マクロとドメイン固有言語へと進めながらLispの概念を説明する記事 (www.defmacro.org/ramblings/lisp.html)。
- Planet Lisp and Planet Scheme -- Lispブロガーの集約サイト(planet.lisp.org(リンク)、 scheme.dk/planet(リンク))。
- schemers.org (リンク) -- Scheme情報のコレクション。
- alien logoは、lisperati.com(リンク)より入手。
著者について
Per Jacobssonは、ロサンゼルスのeHarmony.comのソフトウェア設計者です。仕事で10年Javaを使い、趣味でLispを2年使っています。pjacobsson.com(リンク)からPer Jacobssonに連絡できます。
原文はこちらです:http://www.infoq.com/articles/lisp-for-jvm
(このArticleは2008年7月10日に原文が掲載されました)