ハッシュテーブル表示: pretty print対応版

前回、ハッシュテーブル用にprint-objectメソッドを定義したが、これには重大な欠点があった。

次の例を見てもらえば分かると思うが、要素数がある程度大きくなると出力が極めて見にくくなってしまう。

;; 30要素のハッシュを作成する
> (let ((hash (make-hash-table)))
    (dotimes (i 30 hash)
     (setf (gethash (random 1000) hash) (random 1000))))
--> #<HT (254 . 504) (500 . 888) (121 . 988) (533 . 880) (530 . 699) (559 . 816) (410
                                                                              . 403) (717
                                                                                      . 93) (713
                                                                                             . 43) (339
                                                                                                    . 421) (83
                                                                                                            . 944) (714
                                                                                                                    . 951) (574 ;...延々と続く...
                                                                                                 


これを解決するために、ハッシュテーブル表示関数をpretty-print制御関連の関数を使って実装し直した。

;; ハッシュテーブル用のpprint関数
(defun pprint-hash-table (hash stream &aux (cnt (hash-table-count hash)))
  (print-unreadable-object (hash stream)
    (pprint-logical-block (stream nil :prefix "HT")
      (loop FOR k BEING THE HASH-KEY USING (hash-value v) OF hash DO
        (pprint-pop) ; <= *print-length*や*print-circle*、*print-shared*変数を見て、適切な出力を行ってくれる

        ;; 以下の三行は通常の出力処理
        (write `(,k . ,v) :stream stream)
        (unless (zerop (decf cnt))
          (write-char #\Space stream))     

        (pprint-newline :linear stream))))) ; <= 適切な場所で改行を出力

;;
(defvar *print-hash-table-limit* 32)
(defmethod print-object :around ((obj hash-table) stream)
  (if (<= (hash-table-count obj) *print-hash-table-limit*)
      (pprint-hash-table obj stream) ; pprint-hash-table関数呼び出しに変更
    (call-next-method)))

(コード量は増えたが)実質的にはそれほど変更はなく、ハッシュの要素出力全体をpprint-logical-blockで一つの出力単位としてまとめて、個々の要素出力の後にpprint-newline(改行すべきかどうかを自動で判断してくれる関数)を呼び出すようにしただけだが、これで随分見やすくなった。

> (let ((hash (make-hash-table)))
   (dotimes (i 30 hash)
     (setf (gethash (random 1000) hash) (random 1000))))
#<HT(586 . 749) (482 . 924) (457 . 354) (646 . 403) (715 . 770) (432 . 110)
    (235 . 962) (383 . 509) (557 . 865) (883 . 564) (349 . 928) (658 . 976)
    (381 . 93) (515 . 362) (694 . 182) (759 . 109) (949 . 651) (814 . 910)
    (834 . 433) (155 . 831) (582 . 590) (532 . 565) (410 . 710) (841 . 283)
    (438 . 961) (992 . 473) (365 . 9) (825 . 33) (927 . 505)>

;; *print-length*も適切に扱ってくれる
> (setf *print-length* 3)
> (let ((hash (make-hash-table)))
   (dotimes (i 30 hash)
     (setf (gethash (random 1000) hash) (random 1000))))
#<HT(6 . 783) (117 . 991) (197 . 735) ...>

良い感じ。
出力の見やすさはインタラクティブな開発にとって、実はかなり重要だと思うので、その辺を柔軟に制御できるのはありがたい(かつ面白い)

追記

pretty printに関するドキュメントは(多分)少ないが、『Some Useful Lisp Algorithms: Part 2』という論文(?)の中に、lispのプログラムをpascalのそれに変換するためにpretty printを使う、という章(?)があるので興味のある人は読んでみても良いかもしれない(と云いつつ僕自身はざっと流して読んだだけで、内容をちゃんと把握していないのだが)