Common Lisp で JavaScript の JSON.stringify?

Common LispでJavaScriptのJSON.stringify←→JSON.parseのようなことをする - @peccul is peccu

を見てちょっと気になったので。

乱暴に言うと Common Lisp の出力には2種類あって、正しい呼び方があるような気がするけど知らないので "PRINC出力" "PRIN1出力" と呼んでおく。

;; PRINC出力
* (format t "~A" '(:key "value"))
(key value)
=> nil
* (princ '(:key "value"))
(key value)
=> (:key "value")

;; PRIN1出力
* (format t "~S" '(:key "value"))
(:key "value")
=> nil
* (prin1 '(:key "value"))
(:key "value")
=> (:key "value")

format で出力する場合は "~S" でPRIN1出力、 "~A" でPRINC出力になる。
なにが違うかというと

prin1 produces output suitable for input to read. It binds *print-escape* to true.

princ is just like prin1 except that the output has no escape characters. It binds *print-escape* to false and *print-readably* to false. The general rule is that output from princ is intended to look good to people, while output from prin1 is intended to be acceptable to read.

CLHS: Function WRITE, PRIN1, PRINT, PPRINT...

PRIN1出力が READ で読み込めるような出力であるのに対し、PRINC出力は人間が読みやすいような出力。ということになってる。
その辺をもうちょっと深掘りすると、Common Lisp の printer は printer control variable と呼ばれる変数群でどのように出力するかをコントロールできるようになっていて、PRIN1出力やPRINC出力は前述の意図に沿った形で出力するようにそれらの変数を適切な値にして出力する

printer control variable の中に *print-readably* てのがあって、その名の通り non-nil であれば READ で読み込めるような出力をしなさい。無理ならエラー投げなさい。という変数。

If *print-readably* is true, some special rules for printing objects go into effect. Specifically, printing any object O1 produces a printed representation that, when seen by the Lisp reader while the standard readtable is in effect, will produce an object O2 that is similar to O1. (中略)If printing an object readably is not possible, an error of type print-not-readable is signaled rather than using a syntax (e.g., the ``#<'' syntax) that would not be readable by the same implementation.

CLHS: Variable *PRINT-READABLY*

なので read で読み込めるように出力するには *print-readably* を non-nil にして出力すれば良いということになる。
てっきりPRIN1出力する時は *print-readably* が non-nil になるものと思ってたので、PRIN1出力しとけば良いだろうと思ったのだけど、確認してみたらそうではなかった。

prin1 produces output suitable for input to read. It binds *print-escape* to true.

(中略)

Notes:

The functions prin1 and print do not bind *print-readably*.

CLHS: Function WRITE, PRIN1, PRINT, PPRINT...

なんだよそれ、中途半端なことしやがって・・・。

そいで *print-readably* を non-nil にすると具体的に何が変わるかというと、 #<...> みたいな出力されるもの。これは read に与えるとエラーになる。というか read できないもの用の表現

* (prin1 (make-hash-table))
#<hashtable :test eql :size 0/17 17764932>
=> #<hashtable :test eql :size 0/17 17764932>
* (read-from-string (prin1-to-string (make-hash-table)))
Line 1: ディスパッチングマクロ副文字ではありません: <
;; xyzzy で、リーダーマクロが定義されてないというエラー
;; 真っ当な Common Lisp 処理系だともうちょっと気の利いたエラーになりそう

どう出力されるかは処理系依存だけど、エラーになるか #.でどうにかするか、いずれにしろファイルに出力しておいたけど後で読み込もうとしたら読み込めなくてどーすんだよ!オマエ責任とれよ!みたいなことにはならないはず。

なので、最初に戻って JSON.stringify みたいなこと、というか読み込めるように出力したいのであればこうした方が良さそう。

(defun print-readably (object &optional out)
  (write object :stream out :readably t))

余談: print-object メソッドを自分で実装する人は *print-readably* が non-nil だったら read できるように出力するかエラー投げないといけないとのことなのでガンバレ。

Individual methods for print-object, including user-defined methods, are responsible for implementing these requirements.

CLHS: Variable *PRINT-READABLY*

めんどかったら print-unreadabl-object 使っておけば *print-readably* が non-nil だったらエラー投げてくれる。

Exceptional Situations:

If *print-readably* is true, print-unreadable-object signals an error of type print-not-readable without printing anything.

CLHS: Macro PRINT-UNREADABLE-OBJECT