nptclのブログ

Common Lisp処理系nptの開発メモです。https://github.com/nptcl/npt

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?
確かにそうかも。
作ってみてわかったのですが、確かに線形時間で終わりそうなものです。
もっとバックトラックとかふんだんに使って試行錯誤する 訳の分からないものかと思ってたんですが、 この通り作って行けば、素直に出力できると思います。

初めは何書いてるのかさっぱりわからなくて、 一体どうすりゃいいんだと何日も悩んでたんですけど、 たぶん何とかなりそうです。
ただ、条件付き改行の挙動をちゃんと理解するのはとても難しかったです。
めちゃ苦労しました。
だから本投稿にて文章で残します。