不確定特異点

広く深く、ところどころ超深く

Common Lisp って何がすごいのか?

Common Lisp はマルチパラダイム言語であるとか、高速であるとか、REPL を使ったインタラクティブな開発が可能であるとか、悟りが開けるとかw、いろいろな特徴がありますが、その中でもプログラム可能なプログラミング言語という特徴が他の言語と比較して際立っているように思います。C/C++JavaPerl/Python/Ruby のような他のプログラミング言語では、本質的にはあらかじめ決められた構文しか扱うことができません。それに対して、Common Lisp にはマクロという機構を使ってプログラマが自由に言語を拡張していくことができる仕組みが備わってます。

もちろん C/C++ にもマクロはありますが、本質的には単なる文字列置換にすぎません。Common Lisp のマクロはプログラムを入力にとり、処理を加え、プログラムを出力するといったことが、C/C++ のマクロのプリプロセス処理に相当するフェーズで可能なのです。そのような本物の(?)マクロを使うことで、多彩な構文表現が可能になり、そういう意味で、Common Lisp がプログラム可能なプログラミング言語と言われている所以です。

例えばある整数 a, b を与えて、a から b までインクリメントしながら処理を行うことを考えてみます。C言語だとこのような感じ。(あまりいい例ではありませんが・・・)

for (i = 0; i <= 10; i++)
    printf("%d\n", i);

この構文には初期化、終了判定、更新処理が含まれます。このような定型処理をマクロを使って簡略化すると、以下のようになります。

/* マクロ定義 */
#define FOR(x,a,b) for (x = a; x <= b; x++)

/* マクロを利用 */
FOR(i, 0, 10)
    printf("%d\n", i);

文字数が減って、ちょっとは、すっきりしたかもしれません。

一方、Common Lisp 版です。まずはマクロを使わない書き方。

(do ((i 0 (1+ i)))
    ((> i 10))
  (print i))

これをマクロを使って書き直します。

;; マクロ定義
(defmacro for! ((x a b) &body body)
  `(do ((,x ,a (1+ ,x)))
       ((> ,x ,b))
     ,@body))

;; マクロを利用
(for! (i 0 10)
  (print i))

defmacro というマクロを定義する構文を使って、新たに for! という構文を作りました。確かにこの for! の定義により、実行前にマクロにプログラムが入力され、do 構文に包み込む処理を施し出力する、ということをやっているのですが、これだけだと正直、C言語のマクロと大差なく、単なる文字列置換のように思えます。Common Lisp のマクロの利点って何だろう?

では、次のように繰り返し範囲の最後を表す値が重たい処理(very_long_calculation)を必要としている場合。

FOR(i, 0, very_long_calculation())
    printf("%d\n", i);

これは次のように展開されます。

for (i = 0; i <= very_long_calculation(); i++)
    printf("%d\n", i);

展開結果を見るとわかりますが、これは for ループの終了判定をするたびに、毎回 very_long_calculation() が実行されてしまうことを意味します。無駄ですね。これを回避するためには、very_long_calculation() を一度だけ求めて、for 文の終了判定に使えばよいのですが、C言語のマクロで書くのは難しいです。

Common Lisp ではこれがいとも簡単に書けてしまいます。

;; マクロ定義
(defmacro for ((x a b) &body body)
  (let ((to (gensym)))
    `(let ((,to ,b)) 
       (do ((,x ,a (1+ ,x)))
           ((> ,x ,to))
         ,@body))))

マクロの利用とその展開形は以下のようになります。

;; マクロを利用
(for (i 0 (very-long-calculation))
  (print i))

;; マクロ展開形
(LET ((#:G940 (VERY-LONG-CALCULATION)))
  (DO ((I 0 (1+ I))) 
      ((> I #:G940)) 
    (PRINT I)))

細かい説明は割愛しますが、冒頭の let で (very-long-calculation) を評価した結果 (G940 というシンボルに代入されます) を do の繰り返し条件に用いるような構文に展開されてます。これでめでたく、ループするたびに何度も重たい計算をする必要がなくなりました。よかったですね。

これだけだと Common Lisp のマクロの利点があまり浮き彫りにならないかもしれませんが、重要なことは、プログラムを生成するプログラム(=マクロ)を作ることが出来る!という点です。Lisp はプログラムとデータ構造がともに S 式であるため、S 式を生成するプログラムが S 式で書ける能力があります。それはすなわち、マクロを定義することにより、プログラムを生成するプログラムを書いていることに他ならないのです。さらに言うと、マクロを定義するマクロを書くことにより、プログラムを生成するプログラムを生成するプログラムを書けちゃうわけです。この万能感が半端ねぇっす!