孤独にそっくり

開いている窓の前で立ち止まるな

実践Common Lispを読む 第4章〜第6章

Slime
鎮座するスライム。

こんにちは。
今回は第4章〜第6章まで読みました。
内容が濃く、噛み砕いて書こうとすると分量が大変なことになるので、かい摘んでまとめていきたいと思います。

第4章 シンタックスとセマンティクス

シンタックス=文法、セマンティクス=意味、くらいでいいでしょうか。
Lispシンタックスは「Lots of Irritating Superflous Parentheses(過剰でイライラさせる大量の括弧)」と蔑まれているが、Lisperには好まれています(?)。
その秘密に迫るために、Lispの仕様について詳しく見ていくということです。

*言語処理系の動作のブラックボックス

普通の言語のインタプリンタやコンパイラブラックボックスが1つで、中身は字句解析器→抽象構文木→評価器みたいなっていますが、Lispは2つのブラックボックス読取器評価器)があります。

  • 読取器:文字列をどのようにS式(評価前の式そのものor読取結果)へ変換するか決める
  • 評価器:Lispのフォームを決める(S式が全てLispフォームではないから)

例:(foo 1 2) ("foo" 1 2) どちらもS式だが、前者だけがLispフォームとして意味がある

*関数呼び出し

基本式は

(function-name argument*)

S式では、最初にargumentを評価して、それからfunction-nameに引き渡す
それさえわかれば下の式の順序もわかりやすい

(* (+ 1 2) (- 3 4))
>-3

前置記法でも怖くない

*特殊オペレータ

関数みたいに考えると次の場合にxをうまく評価できない
なぜならxもargument*に含まれてしまうから

(if x (format t "yes")(fromat t "no"))

でも、これは特殊オペレータだから大丈夫らしい
特殊なオペレータは25種類
ifもそのうちの一つで、次のように使う

(if test-form then-form [else-form])

QUOTEも特殊オペレータ

(quote (+ 1 2))='(+ 1 2)
>(+ 1 2)

LETも

(let ((x 10)) x)
>10
*マクロ

マクロはS式を引数として取り、そのマクロフォームのかわりに評価されるLispフォームを返す関数である(?)
マクロフォームの評価は2段階
1.マクロフォームが評価されないままマクロ関数に渡される
2.マクロ関数が返すフォーム(マクロ展開)が通常の評価手順で評価される

マクロフォームの要素はちゃんとしたLispフォームでなくてよい
なぜなら、マクロは自分自身にローカルなシンタックスを定義するから
つまり、関数に似てるけどマクロは全然違うことをやっているよ!ってことらしい…

*真、偽

NILが唯一の偽の値(「()」は読取器でnilと読み取るらしい

*等しさ

eq,eql,equal,equalpの4つがあるけど、「いつでもeqlを使え」
equalは等価性の判断基準が甘くてequalpはもっと甘い

Lispコードの書式

書式について心がけること

  • ネストしたらインデントをひとつ深くする:インデントの間違いでコードのミスがわかるからいいよ
  • 括弧は独立せず行末で閉じる(「)))」みたいのを気持ち悪がらない。括弧は目立たないほうがいい)

第5章 関数

Common Lispは正確には関数型言語ではないし、Lisp系で最も「純粋な」関数型に近いSchemeでさえも、HaskellやMLに比べて純粋さが足りないらしい。

*defun
(defun name (parameter*)
 "省略可能なドキュメンテーション文字列"
body-form*)

nameはだいたいアルファベットとハイフンで書く
ある値を変換するなら「->」を使うこともある

*オプショナルパラメータ

初期値をセットして、それが使われているかいないか知りたいとき

(defun foo (a b &optional (c 3 c-supplied-p))
  (list a b c c-supplied-p))
(foo 1 2 ) -> (1 2 3 NIL)
(foo 1 2 3) -> (1 2 3 T)
(foo 1 2 4) -> (1 2 4 T)
*レストパラメータ

オプショナルパラメータを全部定義するのが大変な時は可変な&restを使えばいい

*キーワードパラメータ

&keyすると位置に依存しなくて便利

パラメータの併用は予期せぬ事態を起こすことがある

*データとしての関数

defunには

  • 関数に名前をつける
  • 関数オブジェクトの生成をする

という二つの役割がある

関数オブジェクトを通じて関数を起動するにはfuncallapplyがある

  • funcallは引数の個数が明確な場合に使い、最初の引数を起動する関数オブジェクト、それ以降を引数とする
  • applyは最初の引数はfuncallと同じだが、リストを扱える
;以下は同義
(foo 1 2 3)=(funcall #'foo 1 2 3)

;関数オブジェクトfnを受け取って、そこで生成した値だけ*をプロットする
(defun plot (fn min max step)
           (loop for i from min to max by step do
                (loop repeat (funcall fn i) do (format t "*"))
                (format t "~%")))
*無名関数
(lambda (parameters) body)

lambdaは関数オブジェクトの名前そのものだと考えられる

(funcall #'(lambda (x y) (+ x Y)) 2 3)->5

lambdaはクロージャを作成できる

第6章 変数

Common Lispは動的型付け
Common Lispにおける値は概念上は、全てオブジェクトへの参照
Lispは関数が呼ばれるたびに、その呼び出し元から渡された引数を保持するための新しい束縛(binding)を作る

*let
(let (variable*)
 body-form*)

(let ((x 10) (y 20) z)
  ...)

各変数の初期化。Letは、xは10、yは20、zはNILに束縛する
関数のパラメータやLETの変数のスコープは、変数を導入したフォームによって束縛される
最も内側の変数の束縛が外側の束縛を隠す(shadow)

*レキシカル変数とクロージャ

クロージャは変数ではなく、束縛が補足される

(defparameter *fn* (let ((count 0)) #'(lambda () (setf count (1+ count)))))

1つのクロージャには複数の変数の束縛も閉じ込められる

(let ((count 0))
 (list
    #'(lambda () (incf count))
    #'(lambda () (decf count))
    #'(lambda () count))))
*ダイナミック変数

defparameterは評価されるたびに常に初期値を代入
defvarは変数が未定義だった場合に初期値を代入する(なくてもいい)

*代入

SETFの基本的なフォーム

(setf place value)

(defun foo(x) (setf x 10))

(let ((y 20))
  (foo y)
  (print y))

>20
>20

letに束縛されている
setfに関して
束縛に新しい値を代入しても他の束縛には何の影響も与えない。
代入前に束縛に保存されていた値にも何の影響もしない。

*一般化代入

どんなデータ構造でもsetfが使えるぞ

(setf x 10);単純な値                                                                                  
(setf (aref a 0) 10);配列                                                                             
(setf (gethash 'key hash) 10);ハッシュテーブル                                                        
(setf (field o) 10);'field'というスロット                                                             
*モディファイマクロ
(incf x) = (setf x (+ x 1))
(decf x) = (setf x (- x 1))
(incf x 10) = (setf x (+ x 10))

こんなこともできる

(incf (aref *array* (random (length *array*))))

副作用を持つかもしれない部分が一度だけ評価されるから、配列のある要素を気ままに増加させられる
setfで書き直すとちょっと大変

;radomで生成する値が同じになるかわからないからtmpに置いてる                                           
(let ((tmp (random (length *array*))))
  (setf (aref *array* tmp) (1+ (aref *array* tmp))))

他のモディファイマクロ

;場所同士で値を入れ替えるrotatef                                                                      
(rotatef a b)
=
(let ((tmp a)) (setf a b b tmp) nil)

;左にずらすshiftf 最後の引数は最後から2番めの引数へと移動される値を提供                              
(shiftf a b 10)
=
(let ((tmp a)) (setf a b b 10) tmp)

まとめ

納得しながら読めたところと、理解が微妙なところがありましたが、ひとまず6章まで読みました。
それにしても、各話題に関して、かなり詳しく書いてあるので、リファレンスとしても使えそうな気がします。
特に面白かったのは、第4章のブラックボックスらへんです。なぜNILと'()が同じ意味なのか、ちゃんと解説されていてなるほど、と思いました。
けれど、クロージャはわかったようなわからないような…
内側の束縛が外側の束縛を隠すのはわかるんですが、漠然とした違和感がありますね。
あと、モディファイマクロも、パッと見では感覚的に理解できるのですが、setfに書き直すと複雑で驚きました。
とりあえず、メモ的な感じなので、わからなかったら本を読んでください。
次回はマクロです。