pprint-newlineの使い方
Common Lispで、いまいちよくわからない「条件付き改行」の使い方を見て行きます。
条件付き改行とは、Pretty Printingの機能の一つであり、
名前の通り条件よって改行するかどうかが決まるものです。
この機能はpprint-logical-block
マクロの中で、
pprint-newline
関数を使うことで実行できます。
簡単に例を挙げるとこんな感じ。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 80) (pprint-logical-block (nil nil) (princ "AAA") (pprint-newline :linear))
条件付き改行は4種類ありますが
通常の改行であるterpri
も特別扱いされますので、
下記の5種類の改行について説明してきます。
:linear
:miser
:fill
:mandatory
terpri
さっそく説明に行きたいのですが、 その前にPretty Printingの基本である 下記の用語の説明をしていきます。
- 論理ブロック
- セクション
- マイザ書式
論理ブロック
論理ブロックとは、pprint-logical-block
マクロで囲んだところです。
これは簡単ですね。
例えばこんな感じ。
(pprint-logical-block (nil nil) (princ "AAA") (princ "BBB"))
セクション
セクションとは、条件付き改行で区切った範囲を表します。
セクションの範囲は条件付き改行に付随するものであり、
このセクションで表される範囲によって
実際に改行が出力されるかどうかが決定されます。
まずは範囲がどのように決定されるかを見て行きましょう。
次の例を考えます。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 80) (pprint-logical-block (nil nil) (princ "AAA") (pprint-newline :linear) (princ "BBB") (pprint-newline :linear) (princ "CCC") (pprint-newline :linear) (princ "DDD") (pprint-newline :linear))
出力結果は次のようになります。
AAABBBCCCDDD
出力例では、条件付き改行の条件が当てはまらなかったため、 一つも改行が出力されませんでした。
ではどの場所で条件付き改行が実施されたかを見たいので、 次のように数値で表してみます。
1 2 3 4 AAABBBCCCDDD
セクションは、自分の改行から見て、前の改行と後ろの改行に挟まれた範囲になります。
数値で範囲を表してみます。
1 2 3 4 AAABBBCCCDDD 111111 222222 333333 444
セクションの決定には色々と複雑な条件があるのですが、 本投稿では全部無視して上記の定義で説明します。
マイザ書式
「マイザ、miser」とは日本語で「けち」を意味します。
例えば、横に長い出力の例を考えましょう。
出力が長くなり、画面右端に近くなった場合は、
なんとか狭いスペースで出力したいと考えるかもしれません。
そこで可能な限り横幅をケチって
縦長に表示したい、というのがマイザ書式です。
ではどういう時にマイザ書式になるのでしょうか。
それは論理ブロックの開始位置が、画面の右端から
*print-miser-width*
変数までの間に入っているときです。
画面の横幅は*print-right-margin*
であり、
マイザ書式の条件が*print-miser-width*
です。
例えば、
*print-right-margin*
が80
*print-miser-width*
が70
のとき、マイザ書式の基準点は80-70=10
になります。
論理ブロックの開始位置が10
以上であったならばマイザ書式になります。
次の文を考えます。
(setq *print-pretty* t) (setq *print-miser-width* 70) (setq *print-right-margin* 80) (fresh-line) (princ "AAAAA") (pprint-logical-block (nil nil) ...)
この場合、論理ブロックは左から5文字目で始まっているため、
マイザ書式の基準には当てはまりません。
しかし、10を超えればいいので、
(setq *print-pretty* t) (setq *print-miser-width* 70) (setq *print-right-margin* 80) (fresh-line) (princ "AAAAABBBBBCCCCC") (pprint-logical-block (nil nil) ...)
これだと論理ブロックが15文字から開始になるので、マイザ書式になります。
ではマイザ書式になると何が変わるのでしょうか?
一つは、インデントが強制的に0になり、設定不可能となります。
もう一つは、条件付き改行で改行しやすくなります。
pprint-newline
関数
条件付き改行はpprint-newline
関数によって出力します。
使い方は次の4通り。
(pprint-newline :linear) (pprint-newline :miser) (pprint-newline :fill) (pprint-newline :mandatory)
省略可能な第二引数により、ストリームを指定できます。
(pprint-newline :linear stream)
この関数は論理ブロック内でしか機能しません。
もし普通の場所で呼ばれた場合は何もしません。
引数の簡単な意味を説明します。
:linear
- 一行に入らなかったら全てを改行する。
:miser
- マイザ書式時に、一行に入らなかったら全てを改行する。
:fill
- 一行に入らなかったら改行するが、可能なかぎり改行しない。
:mandatory
- 一行に入らなかったことにして改行する。
こう書けば簡単に見えるのですが、条件というのがまあ複雑で説明に苦労します。
この4種類の改行と通常のterpri
改行を合わせて
5通りの挙動について説明していきます。
条件付き改行:mandatory
上から順番に説明して行けたらいいのですが、
まずは下の方にある:mandatory
から。
この改行は、次の二通りの意味を持ちます。
- 必ず改行を出力する。
- セクションが一行に収まらなかったことにする。
:mandatory
は改行が必ず出力されるので、
改行を挿入したいときに使うのかと考えるかもしれませんが、
そんなことよりも二つ目の意味が強烈です。
「セクションが一行に収まらなかった」という条件は、
他の条件付き改行である:linear
, :miser
, :fill
の判定で使うものです。
:mandatory
は同じ論理ブロックに配置されている
他の条件付き改行に全部対して、
「セクションが一行に収まらなかった」という報告を出します。
言い換えるなら、他の条件付き改行に対して一斉に改行指示を出す、
ということになります。
ただし、他の条件付き改行を絶対に改行させるという訳ではなく、
さらに固有の条件を満たしたときだけに出力されます。
例えば:linear
は:mandatory
により絶対に改行しますが、
:miser
と:fill
は他の条件を満たさないと改行しません。
通常の改行terpri
通常の改行であるterpri
は、:mandatory
と似ています。
terpri
があった場合は、無条件に改行を出力しますし、
同じ論理ブロックの全ての条件付き改行に改行指示を出します。
では:mandatory
との違いは何でしょうか?
terpri
関数はインデントを出力しません。
もし条件付き改行が改行を出力した場合は、
次の行の開始からper-line-prefix
とインデントの空白が出力されます。
それに対してterpri
関数の改行の場合は、per-line-prefix
しか出力されません。
条件付き改行:linear
条件付き改行の基本となるのが:linear
です。
:linear
は、次の条件の時に改行が挿入されます。
- セクションが一行に収まらなかったとき
- 同じ論理ブロックに
:mandatory
があるとき
もし条件に当てはまった場合は、その改行が一つだけ挿入されるわけではなく、
同じ論理ブロックに含まれる:linear
の条件付き改行が一斉に全部改行されます。
つまり同じ論理ブロック中にある:linear
は、
全てが改行されるか、あるいは全て何もしないかのどちらかになります。
次の例を考えます。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 80) (pprint-logical-block (nil nil) (princ "AAAA") (pprint-newline :linear) (princ "BBBB") (pprint-newline :linear) (princ "CCCC") (pprint-newline :linear))
この例では全体の出力が80文字以内に余裕で収まるため、 全ての条件付き改行は無視されて次のような出力となります。
AAAABBBBCCCC
では次の場合はどうでしょうか。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 11) (pprint-logical-block (nil nil) (princ "AAAA") (pprint-newline :linear) (princ "BBBB") (pprint-newline :linear) (princ "CCCC") (pprint-newline :linear))
出力幅は11文字に設定しています。
1番目の条件付き改行のセクションはAAAABBBB
で8文字です。
2番目の条件付き改行のセクションはBBBBCCCC
の8文字であり、
開始位置4文字目からだと4+8=12
で11文字に収まらないため、
次のように出力されると考えるかもしれません。
AAAABBBB CCCC
しかしこれは間違い。
:linear
は今回の判定では無関係なものも全て一斉に改行されるため、
実際には次のように出力されます。
AAAA BBBB CCCC
改行の条件にはさらに:madatory
があり、セクションが一行に十分収まる場合でも、
同じ論理ブロック内に:madatory
がある場合は全部改行されます。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 80) (pprint-logical-block (nil nil) (princ "AAAA") (pprint-newline :linear) (princ "BBBB") (pprint-newline :linear) (princ "CCCC") (pprint-newline :mandatory))
実行例は下記の通り。
AAAA BBBB CCCC
条件付き改行:miser
この条件付き改行は:linear
と同じなのですが、
マイザ書式中でないと改行を出力しません。
これから例文も含めて色々と書いていくわけですが、
結局のところ:miser
は:linear
とたった一つを除いて
全て同じだという事を説明しています。
たった一つの違いとは「マイザ書式中じゃなきゃ改行を出力しない」です。
:linear
と同じように動作するので、
:linear
が一斉に改行されたらマイザ書式中であれば:miser
も改行します。
逆に:miser
が一行に収まらなかった場合は、
:linear
は一斉に改行しますし、マイザ書式中であれば:miser
も改行します。
さらに言うと、:mandatory
があるだけで
:linear
とマイザ書式中の:miser
は全部改行します。
分かりづらいのですが、マイザ書式中でなくても、
一行に収まるか収まらないかの判定は:miser
にて行われます。
マイザ書式ではない場合に行われないのは、改行の出力だけです。
下記に例を示します。
(setq *print-pretty* t) (setq *print-miser-width* 30) (setq *print-right-margin* 20) (pprint-logical-block (nil nil) (princ "AAAA") (pprint-newline :miser) (princ "BBBB") (pprint-newline :miser) (princ "CCCC") (pprint-newline :miser) (princ "DDDD") (pprint-newline :linear) (princ "EEEE") (pprint-newline :linear) (princ "FFFF") (pprint-newline :linear))
実行結果は下記の通り。
AAAA BBBB CCCC DDDD EEEE FFFF
例では:linear
周辺で一行に収まらないため、
少なくとも:linear
は全部改行されることは分かると思いますが、
マイザ書式中のため:miser
も同時に全て改行されています。
もしマイザ書式ではなかった場合はどうなるでしょうか。
次に例を示します。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 20) (pprint-logical-block (nil nil) (princ "AAAA") (pprint-newline :miser) (princ "BBBB") (pprint-newline :miser) (princ "CCCC") (pprint-newline :miser) (princ "DDDD") (pprint-newline :linear) (princ "EEEE") (pprint-newline :linear) (princ "FFFF") (pprint-newline :linear))
実行結果は次の通り。
AAAABBBBCCCCDDDD EEEE FFFF
:linear
だけが全て改行され、:miser
は改行が出力されませんでした。
条件付き改行:fill
この条件付き改行は、可能な限り横に列挙することを目的とします。
もし一行に収まらなかった場合、:linear
では一斉に改行されましたが、
:fill
ではセクションが超過したもののみ改行を行い、他の:fill
は改行されません。
もし:fill
のセクションが一行に収まらない場合は、
:linear
と:miser
の場合と同様に、
同じ論理ブロックに配置している:linear
と:miser
も
改行条件を満たすことになります。
つまり、:fill
が一行に収まらない時は:linear
は全て改行し、
マイザ書式中であれば:miser
も全て改行します。
一方、:linear
などでセクションの横幅が超過した場合は、
他の条件付き改行も一斉に改行するのですが、
:fill
に関しては無関係であり影響を受けません。
次の例を考えます。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 11) (pprint-logical-block (nil nil) (princ "AAAA") (pprint-newline :fill) (princ "BBBB") (pprint-newline :fill) (princ "CCCC") (pprint-newline :fill))
実行結果は次の通り。
AAAABBBB CCCC
この例文は、:linear
の例を全て:fill
に変えたものです。
:linear
の時は全てが改行されましたが、
:fill
の場合はなるべく一行に収めようとして、
はみ出る場合にのみ改行をしているのがわかります。
:fill
にはさらに言っておかなければいけないことが色々とあります。
:mandatory
だとどうなるでしょうか?
:mandatory
は、セクションが一行に収まらなかったことにする意味があると書きました。
しかし:fill
の場合には当てはまらず、
もし:mandatory
が存在しても:fill
の挙動は変わりません。
実行例を見て行きます。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 11) (pprint-logical-block (nil nil) (princ "AAAA") (pprint-newline :fill) (princ "BBBB") (pprint-newline :fill) (princ "CCCC") (pprint-newline :mandatory))
実行結果は次の通り。
AAAABBBB CCCC
:mandatory
があっても:fill
の挙動が変化していないのがわかります。
ただし例外があるのです。
:fill
は、マイザ書式中だと:linear
と全く同じ意味になります。
よって:fill
のセクションが一行に収まらなかったら
他の条件付き改行も巻き込んで一斉に改行するし、
:linear
と:miser
と:mandatory
が改行した場合は
つられてマイザ書式中の:fill
も改行します。
次の例を示します。
(setq *print-pretty* t) (setq *print-miser-width* 100) (setq *print-right-margin* 11) (pprint-logical-block (nil nil) (princ "AAAA") (pprint-newline :fill) (princ "BBBB") (pprint-newline :fill) (princ "CCCC") (pprint-newline :mandatory))
実行結果は次の通り。
AAAA BBBB CCCC
マイザ書式なので:fill
が:miser
と同じ動きになっているのがわかります。
さらに次の例を示します。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 15) (pprint-logical-block (nil nil) (princ "AAAA") (pprint-newline :linear) (princ "BBBB") (pprint-newline :fill) (princ "CCCC") (pprint-newline :fill) (princ "DDDD") (pprint-newline :fill) (princ "EEEE") (pprint-newline :fill))
実行結果は次の通り。
AAAA BBBBCCCCDDDD EEEE
この例では、:fill
のセクションが一行に収まらなかったため、
:linear
にも影響が出て改行されています。
:fill
は一行に収めようとする挙動自体は変わらないのですが、
論理ブロック全体に改行の条件が加わるため:linear
にも影響が出るのです。
改行の連鎖
次の出力はどうなるかわかるでしょうか。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 100) (pprint-logical-block (nil nil) (princ "AAA") (pprint-newline :linear) (princ "BBB") (pprint-newline :linear) (pprint-logical-block (nil nil) (princ "CCC") (pprint-newline :linear) (princ "DDD") (pprint-newline :linear) (pprint-logical-block (nil nil) (princ "EEE") (pprint-newline :linear) (princ "FFF") (pprint-newline :linear) (princ "GGG") (pprint-newline :mandatory))))
論理ブロックが3つネストしており、
一番最後に:mandatory
で強制改行をしています。
最後のブロックだけが強制的に改行されるので、
次のようになると思うかもしれません。
AAABBBCCCDDDEEE FFF GGG
しかしそうではなく、全部改行されます。
実行結果は下記の通り。
AAA BBB CCC DDD EEE FFF GGG
なぜこうなるのでしょうか?
論理ブロックの:linear
の考え方を思い出しましょう。
もし:linear
のセクションが一行に収まらなかったら、
同じ論理ブロックの全ての:linear
が一斉に改行されました。
そのあとに:miser
も:fill
も改行されることがあると言いました。
これは論理ブロック内にある出力関数が、
一つでも改行した場合にはすべてを一斉に改行させる
という機能によるものです。
条件付き改行が一つでも改行したら他の条件付き改行も同時に改行しますが、
改行の出力は必ずしも「条件付き改行」じゃなくても構いません。
terpri
であったり、format
でもいいわけです。
もっと言うと、並列に配置してあるpprint-logical-block
でもいいわけです。
では:linear
と同じ論理ブロックに配置されている
別の論理ブロックが改行した場合はどうなるでしょうか?
別の論理ブロックの改行が契機となって:linear
が一斉に改行します。
じゃあ別の論理ブロックに配置されている、
さらにネストされた別の論理ブロックが改行したら?
答えは例文で示した通り、ネストされた論理ブロックで出力された改行が
元の論理ブロックへと次々と伝搬していきます。
結果、条件付き改行は全てが改行されたわけです。
では、今回の例のように連鎖のきっかけとなる改行は何でしょうか?
答えはterpri
か:mandatory
のどちらかだけとなります。
不思議に思うかもしれませんが、
例えば、ネストされた:linear
では改行の連鎖のきっかけにはなりません。
例を示します。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 20) (pprint-logical-block (nil nil) (princ "AAA") (pprint-newline :linear) (princ "BBB") (pprint-newline :linear) (pprint-logical-block (nil nil) (princ "CCC") (pprint-newline :linear) (princ "DDD") (pprint-newline :linear) (pprint-logical-block (nil nil) (princ "EEE") (pprint-newline :linear) (princ "FFF") (pprint-newline :linear) (princ "GGG") (pprint-newline :linear))))
横幅20文字に対して
AAABBBCCCDDDEEEFFFGGG
の横幅21文字を出力しようとしています。
入りきれないのでどこかで改行するわけですが、
どこで改行するかわかるでしょうか。
出力結果は下記の通り。
AAA BBB CCCDDDEEEFFFGGG
見てわかる通り、ネストされていない 一番最初の論理ブロックで改行してしまうため、 連鎖して改行するということがありえないのです。
繰り返しになりますが、以上の結果から覚えておかなければいけないことは、
terpri
と:mandatory
はネストされた論理ブロックを巻き込んで
一斉に改行させる、ということです。
規約を見ただけでは、この動作を作り込むのは困難だと思います。
format
による条件付き改行
最後に条件付き改行のformat
の話をします。
format
の書式では、条件付き改行~_
が用意されています。
こんな感じ。
:linear ~_ :miser ~@_ :fill ~:_ :mandatory ~:@_
さらに言うと、pprint-logical-block
も用意されています。
~< ... ~:>
こちらは引数を1つ取ります。
pprint-logical-block
の~<
は非常に多機能で
色んなことができるのですが説明しません。
次の例文を考えます。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 20) (pprint-logical-block (nil nil) (princ "AAAA") (pprint-newline :miser) (princ "BBBB") (pprint-newline :miser) (princ "CCCC") (pprint-newline :miser) (princ "DDDD") (pprint-newline :linear) (princ "EEEE") (pprint-newline :linear) (princ "FFFF") (pprint-newline :linear))
format
文で書き換えると次の通り。
(setq *print-pretty* t) (setq *print-miser-width* nil) (setq *print-right-margin* 20) (format t "~<AAAA~@_BBBB~@_CCCC~@_DDDD~_EEEE~_FFFF~_~:>" '(nil nil))
出力結果は下記の通り。
AAAABBBBCCCCDDDD EEEE FFFF
format
文は相変わらず何書いてあるんだが全然わからん。
このアルゴリズムについて
Pretty Printingは、次の論文がもとになっているとのこと。
22.2.3 Notes about the Pretty Printer's Background http://www.lispworks.com/documentation/HyperSpec/Body/22_bc.htm XP. A Common Lisp Pretty Printing System https://dspace.mit.edu/handle/1721.1/6503
見てはいませんが、cltl2にそのまま書き写されたような感じでした。
次のような文があったのを何となく覚えています。
27.1. Introduction https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node254.html Implementation note: ...XP uses a highly efficient linear-time algorithm.
highly efficient?
確かにそうかも。
作ってみてわかったのですが、確かに線形時間で終わりそうなものです。
もっとバックトラックとかふんだんに使って試行錯誤する
訳の分からないものかと思ってたんですが、
この通り作って行けば、素直に出力できると思います。
初めは何書いてるのかさっぱりわからなくて、
一体どうすりゃいいんだと何日も悩んでたんですけど、
たぶん何とかなりそうです。
ただ、条件付き改行の挙動をちゃんと理解するのはとても難しかったです。
めちゃ苦労しました。
だから本投稿にて文章で残します。