プログラミングを頑張る日記

プログラミングを勉強して、ハッカーになろう

Common Lispを頑張る(40)

本日は久しぶりに「実践Common Lisp」です。7章、マクロについての話が始まります。

マクロについてそろそろ学ばねばなりません。
世の中のLisper達は口を揃えてLispの力はマクロにあると言い、
マクロこそが他の言語とLispを分ける決定的な差であるというのは広く万人の知るところであります。
多分。

そもそもマクロとは? それを考えるために一旦、プログラミング言語が拡張性を提供することについて見てみます。

言語の定義の中には、標準ライブラリが含まれていることは普通のことです。
標準ライブラリは、言語の核を持って実装されます。
それゆえもし標準ライブラリが実装されていなくとも、プログラマは言語の上に同じ機能を実装できます。

言語を、「コアに標準ライブラリを実装したもの」と定義する利点として、理解や実装が容易になること、
そしてなにより、言語が容易に拡張できる表現力というメリットがあるそう。
"言語"だと思っているものの大半がただのライブラリでしかないのです。
「こんな関数が欲しいけどないな」と思ったら自分で追加すればいいのですね。
Javaなんかでも言語の大事な部分はクラスとして定義されていたりして、そこを拡張できます。
...関数を追加することを機能の拡張だと意識したことはありませんでしたが、確かにそうですね。

Common Lispでも関数、クラスで言語を拡張することはサポートされています。
しかし別の方法があり、それこそマクロだそうです。
マクロでは受けとったいくつかのS式をどうやってLispフォームに変換するかを決定することにより、
そのマクロ独自のシンタックスを定義することができるそう。
FormatやLoopのことを思い出せば、かなり好き勝手できるであろうことは想像にかたくありません。
マクロが言語のコアの一部であることで、新しいシンタックスをコアに組込むことなく標準ライブラリとできると。

なるほど。マクロの凄さ、そしてLisp方言の多さの理由、標準のCommon Lispの進化が止まったままの理由がわかった気がします。
軽口を叩いてみました。次からは例として実装されているマクロを見ていきます。

WHENとUNLESS

条件式の一般的なフォームは、IF特殊オペレータによって提供されています。

CL-USER> (if (> 3 2) "ok" "no")
"ok"
CL-USER> (if (> 2 2) "ok" "no")
"no"
CL-USER>

ただ、ご存知の通り、ifはちょっと使い勝手が悪いところがあります。
一つの条件に一つの式しか評価してもらうことしかできないのです。
prognを使えばいいのですが、いちいち書くのも面倒。というわけでマクロの出番です。
仮にWHEN達がなかったとしても、次のように書けるそう。

(defmacro when (condition &rest body)
  `(if ,condition (progn ,@body)))
(defmacro unless (condition &rest body)
  `(if (not ,condition) (progn ,@body)))

ふむふむ。

これらは些細なマクロであり、でもその些細さこそが大事なポイントだそうです。
マクロシステムが言語に含まれているからこそ、このように小さいけど実益のあるマクロを書くことができる。
マクロは決して怖くないのです。マクロは友達。

COMD

まだまだifには我慢できないところがありますね。
条件分岐が複数あって、そのうえ一つの条件で複数の処理を行ないたいときです。
書けなくはないですが、読みづらくなることは明白です。
そこでマクロCONDが用意されています。

CL-USER> (cond ((> 2 2) (princ "ひとつめ"))
               ((> 3 2) (princ "ふたつめ") (princ "ですぞ")))
ふたつめですぞ
"ですぞ"
CL-USER>

本体部分の各要素が1つの条件分岐を表わしていて、それぞれの条件分岐は条件フォームと
その分岐が選ばれたときに評価される0個以上の数のフォームから構成されます。
分岐条件は真になるものが表われるまで順に評価されていき、分岐すると条件フォーム以外の式が評価されていきます。
最後のフォームの値がCONDフォーム全体の結果として返されます。
もし条件分岐が他にフォームを持っていなければ、代わりに条件の値が返されます。
慣習として最後の条件分岐はtという条件で表記されます。
上の例ではtを入れわすれていました。油断。

AND,OR,NOT

ここまででてきた条件分岐のフォームを書くときに便利なのが、AND,OR,NOTの論理演算子です。
といいつつ、NOTは関数だけど関係が深いから一緒に紹介しているそうです。
今更例を出すほどでもありませんね。
ANDとORは条件分岐の代わりにも使えることを覚えておきます。

繰り返し

ループ構文も主要な制御構文の1つです。

実は25種類もある特殊オペレータには、直接ループ構文をサポートするものがないそうです。
Lispにおける全てのループ制御構文は、原始的なgotoの機能を提供する2つの特殊オペレータによって形成されたマクロだそう。
その2つが提供する基礎の上に抽象化を何層も重ねて形作られているということ。

さ、その抽象化の最下層はDOというループ構文。これはとても強力ですが、かなり汎用の抽象化なので、機能過剰ぎみ。
そこで提供されるのが、DOLISTとDOTIMES。柔軟性はDOほどなくとも、よくある場面で使いやすくなっているとのこと。
これらは、DOループに展開されるマクロであり、これらに満足できなければ自分でDOの上にマクロを作ることもできます。

そしてLOOPです。ループ構文を表現するための本格的なミニ言語を提供してくれるこのマクロは、
「よく使うループを簡潔に表現できる」と好かれたり、「Lispっぽくない」と嫌われたりするそう。どっちの言い分もわかります。
なんにせよLispのマクロの力の好例であります。

DOLIST,DOTIMES

DOLISTは、リストから次々と変数値を取出しながら、リスト全体に対してループします。

CL-USER> (dolist (x '(1 2 3)) (print x))

1 
2 
3 
NIL
CL-USER>

最初の改行はいったい...。細かいところで困惑させてきますね。printの仕様なのでしょうが。
なにはともあれ、リストに含まれる変数は、各ループでxに格納され、本体の処理が行われます。

DOTIMESは、数を数えるときに使えます。

CL-USER> (dotimes (i 10) (print i))

0 
1 
2 
3 
4 
5 
6 
7 
8 
9 
NIL
CL-USER> (dotimes (i 9)
           (dotimes (j 9)
             (format t "~3d " (* (1+ i) (1+ j))))
             (fresh-line))
  1   2   3   4   5   6   7   8   9 
  2   4   6   8  10  12  14  16  18 
  3   6   9  12  15  18  21  24  27 
  4   8  12  16  20  24  28  32  36 
  5  10  15  20  25  30  35  40  45 
  6  12  18  24  30  36  42  48  54 
  7  14  21  28  35  42  49  56  63 
  8  16  24  32  40  48  56  64  72 
  9  18  27  36  45  54  63  72  81 
NIL
CL-USER> 

入れ子にだってできます。

DO

上で紹介したマクロは便利ですが、特定の用途にだけ使えるよう最適化されています。
全てのニーズに応えてくれるものではないのです。欲しい機能に合わない...じゃあ、作るしかないですね。
そのときに元にできるのがDOです。

DOは、任意の数のループ変数を束縛でき、ループの各段階においてその値を変更する方法も完全に制御できるそう。
ループがいつ終了するかを決定するテストを定義でき、
式全体の戻り値を生成するためにループの最後で評価するフォームを与えることもできるということです。
すっごい多機能。これは機能制限版がでるわけです。

基本的なテンプレートは下記。

(do (variable-definition*)
    (end-test-form result-form*)
  statemant*)

各variable-definitionは、ループ変数を導入します。
1つの変数を定義する完全なフォームは、次のような3つの要素を含むリストです。

(var init-form step-form)

init-formがループの開始時に評価され、その結果の値が変数varに束縛されます。
引続き起こるループの繰り返し処理に入る前にstep-formが評価され、その新しい値がvarに代入されます。
step-formは省略でき、した場合はループの本体で新しい値を代入するまで変数の値は変わりません。
init-formを省略すれば、varはnilに束縛されます。
毎回の反復を開始するときには、全てのループ変数に新しい初期値が与えられてからend-test-formが評価されます。
その評価がnilである限り反復が繰り返され、statementが順番に実行されます。
end-test-formが真になったら、result-formsが実行され、その最後の評価値がDO式の値として返されます。
毎回の反復では、全ての変数のstep-formが変数に何らかの値を代入する前に評価され、
つまり、 ループ変数も別なループ変数をstep-form内で参照できるというわけです。

う~ん、使えるかなあ。
とりあえず例を。

CL-USER> (do ((n 0 (1+ n))              ;nはカウンタ
              (cur 0 next)              ;curは初期値0で、以降は前回のnextの値をとる
              (next 1 (+ cur next)))    ;nextは初期値1で、以降はcurつまり前々回のnextと前回のnextの和
             ((= 10 n) cur))             ;nが10、つまり11回目でループ終わり、その時のcurの値を返す
55
CL-USER>

コメントがあっているかな。ところで上の例には本体部分がありません。なくてもいいようです。
テンプレートの中でのend-test-form以外は無くてもいいようです。ただし、括弧は絶対に6つ必要。

LOOP

ここまでのループ用のマクロで満たされないことなんてあるのでしょうか。
Common Lispでは多様なデータ構造がありますが、それらに柔軟に対応する方法を提供してくれるのがLOOP。

LOOPマクロには2種類の風味があり、それは「シンプル」と「拡張」だそう。
シンプルなら、骨組みはこうなります。

(loop
 body-form*)

returnを使ってループを抜けるまで延々とbody-formが評価され続けます。

拡張されたLOOPは、まるで「毛色の違う野獣」だそう。凄い表現です。
ループの慣用表現専用の言語を実装するループキーワードを使う点が異なり、皆がそれを好きという訳ではないそう。
ちょっとDOとの比較です。1から10までの数をリストにします。

CL-USER> (do ((nums nil) (i 1 (1+ i)))  ;numsはからっぽ、iは1から1ずつ増える
             ((> i 10) (nreverse nums)) ;iが11になったら終わり、numsを逆にして返す
           (push i nums))               ;numsの先頭ようそにiを足していく
(1 2 3 4 5 6 7 8 9 10)
CL-USER>

そう難しいものでもありませんが、決して直感的とは言えないでしょう。
さて、LOOPで書きます。

CL-USER> (loop for i from 1 to 10 collecting i)
(1 2 3 4 5 6 7 8 9 10)
CL-USER>

殆ど英語みたいなものです。直感的であり、そしてさくっと書けます。

もっと例を書きます。

CL-USER> (loop for x across "the quick brown fox jumps over the lazy dog"
              count (find x "aiueo"))   ;文字列中の母音を数える
11
CL-USER> (loop for i below 10
              and a = 0 then b
              and b = 1 then (+ b a)
              finally (return a))       ;11番目のフィボナッチ数を計算
55
CL-USER>

やっぱりLOOPは強力ですね。

しかし、それでもこれはただのマクロであり、もし標準ライブラリになかったとしても
自分で実装できるということが強調されています。

次の章からいよいよマクロを実際に書いていきますが、今日はここまでにします。