Edited at

CLtL2: 第27章 プリティプリント その3

More than 5 years have passed since last update.

ここで話を次のフォーマット制御文に移せればよかったのですが、そうはできませんでした。結局なんでそんなにプリティプリントが難しいかと言うと、それだけ複雑なことをしているということなんですね。

普通、プログラミングのアルゴリズムは決定的に書きます。もちろんプリティプリントだっておおざっぱにいえば、環境が決まればあるいは印刷幅がきまれば、決定的になるはずですが、どこで改行してどこは改行しないか、という選択肢が多くあるときに、逐次処理的に考えると、あっこの方法ではもう印刷できないから、その前の印刷済みのところでもっと改行しなければいけなかったとなって、やりなおす、というはめにおちいります。いいかえると、先の改行するしないがあとに影響する。決定を局所的にしかも過去の履歴を考えなくていいというわけにはいかない、というところにあります。

こういうのは立派なAI的問題で、バックトラックを使った推論システムなどで解くわけですが、CLのプリティプリントシステムはそれを何とか制限を入れて、あるいは仕掛けを考えて決定的にしたということなんでしょう。でもそのために条件付き改行である:linear、:miser、:fillが複雑になって、しかも使いどころによってお互いがからんでくるために、書くのが非常に難しくなっているということなんでしょう。

典型的な例が CLtL2にpprint-defunとして載っています。

(defun pprint-defun (list)

;; (defun function-name lambda-list [[{declaration}* | documentation]] {form}*)
;; (defun function-name lambda-list form)
(pprint-logical-block (nil list :prefix "(" :suffix ")")
(write (first list)) ; defun
(write-char #\space)
(pprint-newline :miser) ; (1)
(pprint-indent :current 0)
(write (second list)) ; function-name
(write-char #\space)
(pprint-newline :fill) ; (2)
(write (third list)) ; lambda-list
(pprint-indent :block 1)
(write-char #\space)
(pprint-newline :linear) ; (3)
(write (fourth list)))) ; form

コメントとして、正規の defun の構文とここで前提とされている簡易版での defun の構文を掲げておきました。この例では全体がひとつの論理ブロックとして定義されているだけです。注目していただきたいのは、三つの三種類の条件付き改行 pprint-newline があるということです。

実際に次のような例題で試してみます。

cg-user(2): (setq foo '(defun prod (x y) (* x y)))

(defun prod (x y) (* x y))

そして今までと同様にWindowの幅を色々かえて試してみます。

cg-user(9): (pprint-defun foo)

(defun prod (x y) (* x y))
nil
cg-user(10): (pprint-defun foo)
(defun
prod
(x y)
(* x y))
nil

あれっ。おかしいですね。いつも見慣れたようなパターンに印刷されませんね(これはAllegro Common Lisp 8.1で行った時)。というわけで*print-miser-width*の値を見てみると40になっていました。

cg-user(18): *print-miser-width*

40

そこでこの値をいろいろ変えて試してみると、15のときに何とか違いがでるようになりました。

cg-user(21): (setq *print-miser-width* 15)

15

cg-user(71): (pprint-defun foo)

(defun prod (x y) (* x y))
nil
cg-user(72): (pprint-defun foo)
(defun prod (x y)
(* x y))
nil
cg-user(73): (pprint-defun foo)
(defun
prod
(x y)
(* x y))
nil

印刷幅が十分あるときには、:miserも:fillも:linearも発動されない。下に来るほどWindow幅を小さくしていますが、最初に発動されるのは:linearの改行です。その次に発動されるのは:fillの改行で、最後に:miserです。(ちなみに、:miserが働くとインデンテーションは利かなくなります。)

どうして、そんなふうになるのでしょうか。これが難しい。:linearは:linear条件、:fillは:fill条件、:miserは:miser条件が満たされたときに、改行されるのですが、マニュアルに書いてあることを話としては理解しても、ここを:linearではなく:miserにしたら何が起こるのか、を掴むのが一筋縄ではなく、結局プログラマーが試行錯誤することになりかねません。CLtL2を持っていらっしゃらない方もいるでしょうから、ちょっと長いですが、以下に引用します。


 引数kindが:linearであった場合には、条件付き改行を「リニア書式」で使うことになる。この場合には、表示しようとしたセクション(条件付き改行を直接含むセクション)が1行に収まらないときに、しかもそのときだけに改行が出力される。このキーワードを使うと、論理ブロックに含まれるすべてのリニア書式の条件付き改行の場所で改行が行われるか、また一度も改行が行われないかのどちらかになる。

 引数kindが:miserであった場合には、条件付き改行を「マイザ書式」で使うことになる。この場合にはその条件付き改行がマイザ書式を使う状態にあり、しかもその同じ条件付き改行を含むセクションが一行に収まらないときに、しかもそのときだけに改行が行われる。マイザ書式における条件付き改行の働きはリニア書式のそれに似ているが、マイザ書式が使われる状態にあるときだけ条件付き改行による改行が行われる。マイザ書式が使われる状態にあるときとは、論理ブロックの書きだされる場所から右マージンまでの長さがprint-miser-widthの値以下になったときだけである。

 引数:fillであった場合には、条件付き改行を「フィル書式」で使うことになる。この書式では出力の中に改行が含まれるのは以下のときだけである。(a)(その)条件付き改行に続くセクションが現在表示中の行に収まらないとき、(b)直前のセクションが1行に収まらなかったとき、(c)(その)条件付き改行を直接含む論理ブロック中でマイザ書式を行っており、しかもその条件付き改行を含むセクションが1行に収まらないとき。もし1つの論理ブロックがいくつかのフィル書式の条件付き改行によっていくつかの下位セクションに分割された場合には、基本的にはそれぞれの下位セクションはなるべく1行に収まるように表示される。しかしながら、もしマイザ書式行われているのであれば、フィル書式による条件付き条件付き改行はリニア書式のそれと同じ効果を持つ。


ウーム。これを読んだだけでその動きを理解できる人を私は尊敬してしまいますね。

セクションとは、条件付き改行の前と後ろのことであり、その改行自身もそれを含むセクションに含まれます。セクションは入れ子になっているということです。条件付き改行の場所でテキストをぶった切ったとき、その中に全然改行の可能性がないものをアトム的セクションとして、それらと条件付き改行を含むセクションが入れ子になっている状態です。ただし、今の場合には論理ブロックは一つしかありません。それを図式化すれば以下のようになります。

;; (defun prod (x y) (* x y))

;; 11111 1111 <- miser
;; 1111 11111 <- fill
;; 11111 1111111 <- linear

(defun prod (x y) (* x y))は26文字あります。ですから、それ以上の印刷幅があるときには何も改行されません。それよりちょっとだけ印刷幅が狭くなったとき、何が起きるのでしょうか?

:fillは起こりえません。なぜなら、:fillの改行なしでもdefun prod (x y)までが1行で印刷可能だからです(本当はこういう感覚的な言い方ではなくて、上記のルールにしたがって書かないといけませんが、結果オーライということをご確認下さい)。:miserも起こりません。なぜなら、defun prodが1行に収まるからです。したがって、:liniearだけが起こります。

それでは、(defun prod (x y)が印刷できないほどであったら何が起きるでしょうか?多分コード中では:linearの判断が最初に書いてあるのではないかと思いましたが、ACLでは逆でした。:miserでも:fillでも当てはならないときにその他で:linearになっていました。ただし、どうもコードの中身は上記の記述と完全に等しいとは思われません。ウーム。むずかしいですね。

それにしてもprint-miser-widthが40だとなぜ:fillが起こらないのかがまだ私には不明です。ACL8.1のソースコードを見てみましたが、ちょと短時間ではとても追いきれませんでした。SBCLでは最初プリティプリントができないと思いましたが、パッケージsb-prettyを指定すればいいということがわかりました。

CL-USER(2): (sb-pretty::pprint-linear nil foo)

(NIL
(1ST)
(1ST 2ND)
(1ST 2ND 3RD)
(1ST 2ND 3RD 4TH)
(1ST 2ND 3RD 4TH 5TH)
(1ST 2ND 3RD 4TH 5TH 6TH)
(1ST 2ND 3RD 4TH 5TH 6TH 7TH)
(1ST 2ND 3RD 4TH 5TH 6TH 7TH 8TH)
(1ST 2ND 3RD 4TH 5TH 6TH 7TH 8TH 9TH))
NIL
CL-USER(3): (sb-pretty::pprint-fill nil foo)
(NIL (1ST) (1ST 2ND) (1ST 2ND 3RD) (1ST 2ND 3RD 4TH)
(1ST 2ND 3RD 4TH 5TH) (1ST 2ND 3RD 4TH 5TH 6TH)
(1ST 2ND 3RD 4TH 5TH 6TH 7TH) (1ST 2ND 3RD 4TH 5TH 6TH 7TH 8TH)
(1ST 2ND 3RD 4TH 5TH 6TH 7TH 8TH 9TH))

でも、ACLでできたpprint-defunを動かすことはできませんでした。

CL-USER(4): (defun pprint-defun (list)

(sb-pretty::pprint-logical-block (nil list :prefix "(" :suffix ")")
(write (first list))
(write-char #\space)
(sb-pretty::pprint-newline :miser)
(sb-pretty::pprint-indent :current 0)
(write (second list))
(write-char #\space)
(sb-pretty::pprint-newline :fill)
(write (third list))
(sb-pretty::pprint-indent :block 1)
(write-char #\space)
(sb-pretty::pprint-newline :linear)
(write (fourth list))))

CL-USER(9): (pprint-def

un foo)
(DEFUN PROD (X Y) (* X
Y))
NIL

LispWorksでもできませんね。print-miser-widthはNILでした。これに値を設定しても、状況は変わりません。ほかになにか使うための設定があるのでしょうか?どなたかよく知っていらっしゃる方が調べてもらえればうれしいですね。

何とかプリティプリントを分かりやすくお伝えしようと思って、ここまでやってきましたが、結論としては、やっぱり難しいというのを拭い去ることはできませんでした。

また、チャンスがあったら、再挑戦します。

でもまあ、ACLを用いて、CLtL2に出ている例題をパターンとして、それを若干変更していく方法で、何とかとりくめるという感触は得られたのではないかと期待しています。

次はフォーマット制御文にチャレンジします。