孤独にそっくり

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

実践Common Lispを読む 第18章〜第20章

Cats are practical
practical 猫

こんにちは。
今回は第18章~第20章まで読みました。読んだといっても、よくわかってないのでつらかったです。
重要そうに見える(本当かどうかはわからない)ところを抜き書きしている感じです。

第18章 FORMATの手習い

FORMAT関数はLOOPマクロと並んで、信者とアンチがいる。
FORMAT関数は、3種類の全く違う書式に対応している。データテーブルの出力と、S式のプリティプリントと、人間が読めるメッセージを、値を補完して生成すること。
今回は最後に挙げた機能だけ見る。

*FORMAT関数とは

FORMAT関数は必須引数を2つ取る
最初の引数は出力先でT、NIL、ストリーム、フィルポインタつきの文字列
二つ目は制御文字列。制御文字列はS式ではないので読みづらい。

*FORMAT関数の指示子

すべての指示子はチルダ「~」で始まり、指示子を表す1つの文字で終わる。
指示子によっては前置パラメータを取るものがある。

~n$:小数点以下n桁まで表示(~3$とか)
~v$:vは引数を取り、nの役割を担う
~#$:残りのフォーマット引数の個数として評価される?

指示子によってはコロンやアットマークのような修飾子によって振る舞いを変えるものもある。
~d指定子に「:」をつけると3桁区切り、「@」をつけるとプラス記号がつく。

*基本フォーマット
(format nil "The value is: ~a" 10)

よく使うやつ
~a:最も汎用
~s:READに読み戻せるような出力
~%:常に改行
~&:行頭でないときのみ改行
~c:なんか汎用
~d:整数を10進数(整数は他に~x、~o、~b)

1つ目の前置パラメータで出力の最小値を指定できる
2つ目のパラメータでパディングに使う文字を指定できる

(format nil "~12d" 1000000) -> "     10000000"
(format nil "~12,'0d" 1000000) -> "0000010000000"
;;日付表示
(format nil "~4,'0d-~2,'0d-~2,'0d" 2016 7 17) -> "2016-07-17" 

浮動小数点数は~f、~e、~g、~$の4つ
~f:10進数
~$:~,2fと等価

英語にしてくれるのは~r

(format nil "~r" 1234) -> "one thousand two hundred thirty-four"

~:rは序数で、~@rはローマ数字

~pっていう面白いのもある

CL-USER> (format nil "~r file~:p" 1)
"one file"
CL-USER> (format nil "~r file~:p" 10)
"ten files"
CL-USER> (format nil "~r famil~:@p" 1)
"one family"
CL-USER> (format nil "~r famil~:@p" 10)
"ten families"

複数形にしてくれたりする

*条件による整形

~[~]を使うといろいろできる。

;;引数番目を表示
CL-USER> (format nil "~[cero~;uno~;dos~]" 1)
"uno"
;;区切りが~:;になると、指定したものが無くても最後のが表示される
CL-USER> (format nil "~[cero~;uno~;dos~:;mucho~]" 100)
"mucho"

#を使うともっとすごい

;;2つの~[~]指示子に関して~#で選択している
;;1つめの~[~]は0~2個の引数を消費
;;2つめの~[~]は可能であればもうひとつ消費している
(defparameter *list-etc*
  "~#[NONE~;~a~;~a and ~a~:;~a, ~a~]~#[~; and ~a~:; ~a, etc~].")
;;最後のやつだけカンマが足りてない件
CL-USER> (format nil *list-etc* 'a 'b) -> "A and B."                           
CL-USER> (format nil *list-etc* 'a 'b 'c ) -> "A, B and C."    
CL-USER> (format nil *list-etc* 'a 'b 'c 'd) -> "A, B C, etc."  

;;~[ではなく~:[にすると、2つの節しか含められない
;;9章で使った
(format t "~:[FAIL~;pass~]" test-result
;;~@[だと1つしか含められず、NIL以外のときに実行
*反復

反復指定子~{

(format nil "~{~a, ~}" (list 1 2 3)) -> "1, 2, 3, "
;;最後の, を消したいときは
(format nil "~{~a,~^, ~}" (list 1 2 3)) -> "1, 2, 3"

;;~{の中では、#が参照する値は残りのフォーマット引数の個数ではなく、処理されずに残っている要素の個数
(format nil "~a~#[~;, and ~:;, ~]~}" (list 1 2 3)) -> "1, 2, and 3"
;;上の時、要素が2個のとき、いらないカンマが出てくるので、もっと綺麗に出力する
(format nil "~{~#[~;~a~;~a and ~a~:;~@{~a~#[~;, and ~:;, ~]~}~]~}")

もう読みたくない

*ホップ、スキップ、ジャンプ

~*指示子はリストの中を飛び回れる

;;値を2回使う
(format nil "~r ~:*(~d)" 1) -> "one (1)"

他にも~?指示子や~/指示子やいろいろある。

第19章 例外処理を超えて:コンディションと再起動

Lispのコンディションシステムは、エラー通知と補足の2 つにコードが分かれているJavaなどの例外システムとは違って、コンディショの通知と補足、それに再起動に分割され
ている。

Lispのやり方

Common Lispのエラー処理システムのやり方は、実際にエラーから回復するコードと、どうやって回復するかを決めるコードを分離する。メリットは、どうやって回復するかは高位の関数のコードにまかせて、回復のためのコードは低位の関数に書けることだ。
例として次の状況を想定する。
Webサーバのログのようなテキスト形式のログファイルを読む
parse-log-entry関数:1つのログエントリのテキストに含まれる文字列を受け取り、エントリを表す、log-entryオブジェクトを返す
parse-log-file関数:parse-log-entry関数を呼び出し、ログファイル全体を読み込み、ファイル内の全エントリを表すオブジェクトのリストを返す

*コンディション

malformed-log-entry-error関数:parse-log-entryがパースできない場合に通知するコンディションのクラス
コンディションはDEFINE-CONDITIONマクロで定義するが、クラスと同じ。

;;malformed-log-entry-error関数を定義
;;SLOT-VALUEではアクセスできないからオプションを指定
(define-condition malformed-log-entry-error (error)
  ((text :initarg :text :reader text)))
*コンディションハンドラ

エラーは関数ERRORを使って通知する。ERRORを呼ぶには、事前にインスタンス化したコンディションオブジェクトをERRORに渡すか、コンディションクラスの名前と生成に必要な初期化引数をERRORに渡すかだ。後者だと

(defun parse-log-entry (text)
  (if (well-formed-log-entry-p text)
      (make-instance 'log-entry ...)
      (error 'malformed-log-entry-error :text text)))

コンディションハンドラとかコールスタックのレベルとか難しくてよくわからない…。
とりあえずコンディションハンドラをHANDLER-CASEマクロで確立するらしい。

(handler-case expression
  error-clause*)
;;error-clauseのフォーム
(condition-type ([var]) code)

ひとまず例を見る。

;;HANDLER-CASE式はparse-log-entryの戻り値を返すか、malformed-log-entry-errorが通知された場合にはnilを返す
;;loop内のitは直近に評価された条件式のことなので、entryの値になる
(defun parse-log-file (file)
  (with-open-file (in file :direction :input)
    (loop for text = (read-line in nil nil) while text
	 for entry = (handler-case (parse-log-entry text)
		       (malformed-log-entry-error () nil))
	 when entry collect it)))
;;parse-log-entryが普通に戻ると、その値がentryに代入されLOOPによって集められる
;;しかしerror通知になると、error-clauseはNILを返す。このNILは集められないからスキップされる?

このエラー処理はまだダメなので、次に見る再起動が必要になる…らしい
Javeっぽい例外処理と見比べてみる

;;Java
try{
  doStuff();
  doMoreStuff();
  } catch (SomeException se){
  recover(se);
}

;;Common Lisp
(handler-case
    (progn
      (do-stuff)
      (do-more-stuff))
  (some-exception (se) (recover se)))
*再起動

適切な再起動を実行するコンディションハンドラをアプリケーションの上位に移して、再起動のコードをparse-log-fileやparse-log-entryといった中位や低位の関数に置く。
そのときにはHANDLER-CASEをRESTART-CASEに変える。このとき、名前は挙動を説明するものにする。

;;デバッガの中にskio-log-entryがあり、それを選択するとそのままparse-log-fileの処理が続けられる
(defun parse-log-file (file)
  (with-open-file (in file :direction :input)
    (loop for text = (read-line in nil nil) while text
	 for entry = (restart-case (parse-log-entry text)
		       (skip-log-entry() nil))
	 when entry collect it)))

これ以降図が載ってたりずらずら説明が書いてあるけどさっぱり頭に入ってこないのでパス。実践で使ってあるところを読めば分かるのだろうか…

第20章 特殊オペレータ

特殊オペレータはCommon Lispの理解に役立つ

*評価を制御する

QUOTE:評価を止め
IF:他の全ての条件実行の構文要素の基礎になるブール選択の操作
PROGN:いくつかの式を並べられる
の3つ

*レキシカル環境を操作する

LETやLET*やSETQやFLETやLABELSやMACROLETなど

;;flet:定義した関数の名前は本体でしか使えない
(flet (function-definition*)
  body-form*)
;;labels:再起関数が定義できる
(labels (function-definition*)
  body-form*)
;;function-definition*
(name (parameter*) form*)

FLETとLABELSはクロージャが使える
再起を使うヘルパー関数をグローバル関数として定義したくないときは、LABELESがつかえる。

;;collect-leaves:再起的なヘルパー関数walkで木を巡回してアトムをリストに収集し、それを反転してから返す
;;walk内でleavesを参照している
(defun collect-leaves (tree)
  (let ((leaves ()))
    (labels ((walk (tree)
	       (cond
		 ((null tree))
		 (atom tree) (push tree leaves))
	       (t (walk (car tree))
		  (walk (cdr tree)))))
      (walk tree))
    (nreverse leaves)))

これらはマクロ展開時に使うオペレータとしても役立つ
シンボルマクロってのもある

*ローカルなフローの生後

レキシカル環境における名前の生成や使用にかかわる特殊オペレータ

;;block
;;nameはシンボル、fomrはLispフォーム
;;return-formで脱出しない限り、最後のformがBLOCKの値として返される
(block name 
  form*)
;;DOやDOTIMESには、NILという名前のBLOCKを含んでいる
(dotimes (i 10)
  (let ((answer (random 100)))
    (print answer)
    (if (> answer 50) (return))))

;;TAGBODY:GOで使える名前の定義されたコンテキストをTAGBODYフォームで定義する
;tag-or-compund-formには、シンボル(タグ)か、空でないリストのフォームを置く
(tagbody 
   tag-or-compound-form*)
;;GOにジャンプする
(tagbody
   top
   (print 'hello)
   (go top))
;;tagbodyはあまり使うことはない

スタックの巻き戻しにはCATCHやTHROWやUNWIND-PROTECTとかある

*多値

多値(GETHASHとか)の使用には、多値を返すことと、多値を返すフォームから先頭以外の値を得るという2つの側面がある

;;funcallとmaltiple-value-call
;;funcallは暗黙のうちに2つ目の返り値が捨てられる
;;multiple-value-callは多値
(funcall #'+ (values 1 2) (values 3 4)) -> 4
(maltiple-value-call #'+ (values 1 2) (values 3 4)) -> 10
*EVAL-WHEN

EVAL-WHENを理解するにはLOADとCOMPILE-FILEのやり取りを理解する必要があるらしい。
LOAD:ファイルをロードしてすべてのファイルに含まれるすべてのトップレベルフォームを評価する
COMPILE-FILE:ソースファイルをFASLファイルにコンパイルする
このとき、(load "foo.lisp")と(load "foo.fasl")は等価
LOADは各フォームの値を評価するが、COMPILE-FILEは通常はコンパイルするフォームを評価しない。
EVAL-WHENはコンパイルのタイミングをsituationに列挙された状況との組み合わせで決める。
EVAL-WHENを使う場面は2つある。
ひとつめは、コンパイル時に情報を使って保存して同じファイル中の他のマクロフォームの展開をする場合。→第24章
ふたつめは、マクロの定義とヘルパー関数の定義を、そのマクロを使うコードと同じ置きたい場合。DEFUNはコンパイル時に関数が有効にならないからEVAL-WHENで包む

*その他

LOCALLY,THE,LOAD-TIME-VALUE,PROGV等

ついでに

Emacsのundoとredoを使いやすくする | Linuxとは日記
emacsredoを導入
undo: c-/
redo: c-x u
ちょっと便利

まとめ

オブジェクト指向再入門あたりから、コードの量がガクッと落ちて、文章での説明が増えてきました。
わざわざコードに落とし込まなくてもわかりますよね?ってことなんでしょうけど、僕にはちょっと厳しいです。
ひとまず、次の次の章のLOOPまで読んだら、あとは実践の章がつらつら始まるので、そこで具体例を見ていけるんじゃないかと期待しています。
この本、網羅的に紹介しようとしすぎて詳しすぎィ!ってことがわりとよくあるような…
最初のほうは楽しかったのに、今はプログラミング力皆無の僕にはちょっと読むのがつらいです。