4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

LispAdvent Calendar 2015

Day 15

Common Lisp のプリティプリント

Last updated at Posted at 2015-12-14

昨年のAdvent Calendar もプリティプリントについてでしたが,プリティプリント用マクロや関数までで,~Wのようなフォーマット制御指示子については掲載しませんでした.

以下は電子書籍「Common Lispと人工知能プログラミング」の中からの抜粋です.


プリティプリントとは何か?

ACL with IDE上のREPLにおいて,システムからの印刷がウィンドウ幅によって変化するのは,プリティプリントの働きである.以下の出力を参照されたい.

CommonLisp
cg-user(1): (setq multiplication-table
                   (loop for i from 1 to 9 collect
                         (loop for j from 1 to 9 collect
                               (* i j))))
((1 2 3 4 5 6 7 8 9) (2 4 6 8 10 12 14 16 18) (3 6 9 12 15 18 21 24 27) (4 8 12 16 20 24 28 32 36) (5 10 15 20 25 30 35 40 45)
 (6 12 18 24 30 36 42 48 54) (7 14 21 28 35 42 49 56 63) (8 16 24 32 40 48 56 64 72) (9 18 27 36 45 54 63 72 81))

そのままで,もう一度印刷すると以下の通りだが,

CommonLisp
cg-user(2): multiplication-table
((1 2 3 4 5 6 7 8 9) (2 4 6 8 10 12 14 16 18) (3 6 9 12 15 18 21 24 27) (4 8 12 16 20 24 28 32 36) (5 10 15 20 25 30 35 40 45)
 (6 12 18 24 30 36 42 48 54) (7 14 21 28 35 42 49 56 63) (8 16 24 32 40 48 56 64 72) (9 18 27 36 45 54 63 72 81))

ウィンドウ幅を狭めて印刷すると,以下のようになり,

CommonLisp
cg-user(3): multiplication-table
((1 2 3 4 5 6 7 8 9) (2 4 6 8 10 12 14 16 18)
 (3 6 9 12 15 18 21 24 27) (4 8 12 16 20 24 28 32 36)
 (5 10 15 20 25 30 35 40 45) (6 12 18 24 30 36 42 48 54)
 (7 14 21 28 35 42 49 56 63) (8 16 24 32 40 48 56 64 72)
 (9 18 27 36 45 54 63 72 81))

更に狭めて印刷すれば,次のようになる.

CommonLisp
cg-user(4): multiplication-table
((1 2 3 4 5 6 7 8 9) (2 4 6 8 10 12 14 16 18)
 (3 6 9 12 15 18 21 24 27)
 (4 8 12 16 20 24 28 32 36)
 (5 10 15 20 25 30 35 40 45)
 (6 12 18 24 30 36 42 48 54)
 (7 14 21 28 35 42 49 56 63)
 (8 16 24 32 40 48 56 64 72)
 (9 18 27 36 45 54 63 72 81))

残念ながらSlimeではこうはならない.

また,長いリストや深いリストを印刷するときに「#」とか「..」と省略されるのも,プリティプリントの働きである.ファイルに印刷する場合には,そのような機能はあまり必要がないかもしれないが,REPLでは有効な機能であり,さらにdefun構文やlet構文において,自動的に見やすく行変えとインデンテーションが付けられるのもプリティプリントのおかげである.CLtL1ではユーザがこのプリティプリントを制御する機能はなかったが,CLtL2ではそれが可能になった.

プリティプリントには重要な働きをする大域変数が五つある.

CommonLisp
cl-user(2): *print-pretty*
t
cl-user(3): *print-pprint-dispatch*
#<excl::pprint-dispatch-struct @ #x201a133a>
cl-user(4): *print-right-margin*
nil
cl-user(5): *print-miser-width*
40
cl-user(6): *print-lines*
nil

*print-pretty*の値が真の場合,REPL上の出力は*print-pprint-dispatch*のプリティプリントディスパッチ表に従って印刷される.たとえば,defunとかcondの構文が整形されるのはそのためである.*print-right-margin*は出力幅の右マージンを文字カラム数で与える.これがnilのときは出力ストリームが主力できる最大の1行の長さが用いられる.一方,左のマージンは通常は0であるが,後述のプリティプリントの論理ブロックの中に制限されるし,その中でカウントされる.*print-miser-width*は右マージンまでの残りの文字数がこれ以下になったときに,プリティプリントはコンパクトな書式(マイザー書式)のモードに切り替わる.*print-lines*はプリティプリントが印刷する行数を制限するもので,これを超える時には改行せずに「 ..」(空白と二つのピリオド)に省略して印刷される.既定値はnilであり,それは制限なしを意味する.以下の実施例を見られたい.

CommonLisp
cl-user(7): (let ((*print-right-margin* 40)
                  (*print-miser-width* nil)
                  (*print-lines* 3))
              (pprint '(progn (setq a 1 b 2 c 3 d 4))))

(progn (setq a 1 b 2 c 3 d 4))
cl-user(8): (let ((*print-right-margin* 25)
                  (*print-miser-width* nil)
                  (*print-lines* 3))
              (pprint '(progn (setq a 1 b 2 c 3 d 4))))

(progn (setq a 1
             b 2
             c 3 ..))

上記実施例では,40の印刷幅では1行に印刷できたが,25の印刷幅では小さすぎるためにプリティプリントによって25文字幅に収まるように印刷された例である.

*print-pprint-dispatch*もユーザが既定のプリティプリント形式を修正したり,新しく追加したりできるようになったが,これの取り扱いは本書籍では扱わない.詳細はCLtL2第27章を参照されたい.

CLtL2の第27章にはプリティプリントのためのマクロや関数と,それらを利用したフォーマット文における指示子が説明してあるが,本書籍ではフォーマット文におけるプリティプリントの指示方法について説明する.

フォーマット指示子によるプリティプリント

format文でプリティプリントするときは,指示子として「~W」と「~_」と「~I」を主に使用する.

  • ~W ~Sと似ているがプリティプリント用制御変数の値を受け取るところが異なる.深さカウンタをクリアしないので,プリティプリントの場合にはすべての場所で~Sではなくこれを用いること.
  • ~_ 条件付き改行で,これへの修飾子によってリニア書式,マイザー書式,フィル書式,強制改行書式の指定をする.
  • ~I 綺麗に印刷するための字下げを指示する.その場で字下げをするのではなく字下げの位置をマークして後続の改行時にそれが有効になると考えるべきである.

以下の例ではschemeのある書式に対して「~W」で出力しているが,*print-right-margin*の値によって出力が異なっていることに注目されたい.

CommonLisp
cl-user(2): (setq foo '(define (factorial n) (our-if (= n 1) 1 (* n (factorial (- n 1))))))
(define (factorial n) (our-if (= n 1) 1 (* n (factorial (- n 1)))))
cl-user(3): (pprint foo)

(define (factorial n) (our-if (= n 1) 1 (* n (factorial (- n 1)))))
cl-user(4): (let ((*print-right-margin* 80) (*print-miser-width* nil))
              (format t "~W" foo))
(define (factorial n) (our-if (= n 1) 1 (* n (factorial (- n 1)))))
nil
cl-user(5): (let ((*print-right-margin* 40) (*print-miser-width* nil))
              (format t "~W" foo))
(define (factorial n)
 (our-if (= n 1) 1
  (* n (factorial (- n 1)))))
nil
cl-user(6): (let ((*print-right-margin* 20) (*print-miser-width* nil))
              (format t "~W" foo))
(define
 (factorial n)
 (our-if (= n 1) 1
  (* n
     (factorial
      (- n 1)))))
nil

最後の行番号6の印刷はプリティプリントされてはいるが,デフォールトのプリティプリントの仕方になっていて,defineやour-ifを認識したものではない.以下では上記のフォーマット指定子を使ったより目的別のプリティプリントを考える.

プリティプリントを理解するのに大事な概念がリスト構造に対応した論理ブロックの指定である.フォーマット指示子「~<《制御文字列》:~>」には一つのリストが対応して,プリティプリントのための論理ブロックを形成する.これに対応しないリストは論理ブロックではなくその中の要素にすぎない.「~<《制御文字列》:~>」の制御文字列にはさらに入れ子に「~<《制御文字列》:~>」を入れることができ,これによりプリティプリントに都合のよい多重の論理ブロックを作り上げる.このフォーマット指示子がない場合でも,出力全体が一番外側の論理ブロックとなる.

以下は最も一般的な論理ブロックの使用例である.「~<《プレフィックス》~;《本文》~;《サフィックス》:~>」という形式で本文の前後に任意の文字列を入れることができるし,特に丸括弧については「~:<《本文》:~>」とコロン修飾子をつけて印刷することができる.

CommonLisp
cl-user(7): (let ((*print-right-margin* 80) (*print-miser-width* nil))
              (format t "~<(~;~W ~W ~W~;)~:>" foo))
(define (factorial n) (our-if (= n 1) 1 (* n (factorial (- n 1)))))
nil
cl-user(8): (let ((*print-right-margin* 80) (*print-miser-width* nil))
              (format t "~:<~W ~W ~W~:>" foo))
(define (factorial n) (our-if (= n 1) 1 (* n (factorial (- n 1)))))
nil

一つの論理ブロックの中は条件付き改行指示子「~_」によってセクションに分けられる.今define文の本体の直前でこの論理ブロックを二つのセクションに分割するとする.

CommonLisp
cl-user(9): (let ((*print-right-margin* 80) (*print-miser-width* nil))
              (format t "~:<~W ~W~1I ~_~W~:>" foo))
(define (factorial n) (our-if (= n 1) 1 (* n (factorial (- n 1)))))
nil
cl-user(10): (let ((*print-right-margin* 50) (*print-miser-width* nil))
               (format t "~:<~W ~W~1I ~_~W~:>" foo))
(define (factorial n)
  (our-if (= n 1) 1 (* n (factorial (- n 1)))))
nil

ここで,*print-right-margin*が80の場合には全体が1行で印刷され,50になるとセクション分割により改行されて,それでも関数本体が1行で印刷されていることに留意されたい.これを更に小さく20にすると以下のようになる.下記行番号11では(factorial n)が途中で改行されてしまってあまり美しくない.そこでこれをdefineの直後に改行するように変更したのが行番号12だ.二番目の「~W」の前にコロンで修飾された改行子が挿入されていることに留意されたい.

CommonLisp
cl-user(11): (let ((*print-right-margin* 20) (*print-miser-width* nil))
               (format t "~:<~W ~W~1I ~_~W~:>" foo))
(define (factorial
         n)
  (our-if (= n 1) 1
   (* n
      (factorial
       (- n 1)))))
nil
cl-user(12): (let ((*print-right-margin* 20) (*print-miser-width* nil))
              (format t "~:<~W~3I ~:_~W~1I ~_~W~:>" foo))
(define
    (factorial n)
  (our-if (= n 1) 1
   (* n
      (factorial
       (- n 1)))))
nil

「~《n》I」は現在の論理ブロック中での字下げの位置を指定する.「~I」ではn=0になる.

ただの条件付き改行指示子「~_」はリニア書式指定であり,なるべく全体を1行で印刷しようとする.それができないときに改行するがその場合には,その論理ブロック中のすべてのリニア書式指定の改行が実行される.「~:_」はフィル書式指定であり,その前後のセクションのいずれかで1行で印刷できないときに改行され,前後のセクションをそれぞれなるべく1行に詰め込むように印刷する.「~@_」はマイザー書式を指定するが,それが有効に働く場合は非常に限られており,まず最初にprint-miser-widthが定義されていなければならない.ここでは説明を省略する.どんな場合でも必ずその場所で改行をするのが「~:@_」である.

それでもこれはまだ,our-ifを認識していない.次の実施例を見られたい.ここではour-ifではなく,ifとしている.

CommonLisp
cl-user(17): (setq bar '(define (factorial n) (if (= n 1) 1 (* n (factorial (- n 1))))))
(define (factorial n) (if (= n 1) 1 (* n (factorial (- n 1)))))
cl-user(18): (let ((*print-right-margin* 20) (*print-miser-width* nil))
              (format t "~:<~W~3I ~:_~W~1I ~_~W~:>" bar))
(define
    (factorial n)
  (if (= n 1)
      1
    (* n
       (factorial
        (- n 1)))))
nil

our-ifでなくifの場合にはCommon Lispで定義されたifのプリティプリントが働いてour-ifとは印刷の仕方が異なる.our-ifのプリティプリントをCommon Lispの場合と同様にするには,次のようにする.

CommonLisp
cl-user(19): (let ((*print-right-margin* 20) (*print-miser-width* nil))
              (format t "~:<~W~3I ~:_~W~1I ~_~:<~W~3I ~W ~_~W~1I~^ ~_~W~:>~:>" foo))
(define
    (factorial n)
  (our-if (= n 1)
      1
    (* n
       (factorial
        (- n 1)))))
nil

「~^」はリストの終わりを検出し,その場合にはそこで印刷を打ち切るためのものである.これがないとelse部がない場合にはエラーとなる.

ここまでで,defineとour-ifに対応したformat文によるプリティプリントを明らかにしたが,本当ならばこれらの特別なプリティプリントの仕方をシステムに登録しなければならない.その具体的なやり方はCLtL2の第27章を参照されたい.また,論理ブロック指定を単価記号で修飾した場合には(~@< :~>),format関数への印刷要素がその中の印刷制御指示子に一つ一つ対応付けられるのではなく,印刷されるべき残りの要素がひとまとまりで与えられる.任意個のリスト要素を処理する場合にはこちらを利用しなければならないが,その詳細についても省略する.

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?