The Common Lisp Cookbook – 入出力

Table of Contents

The Common Lisp Cookbook – 入出力

📢 🎓 ⭐ Learn Common Lisp efficiently in videos, by the Cookbook's main contributor. Learn more.

🖊️ Discover a new Common Lisp and Coalton editor for beginners: mine and a new VSCode extension for Common Lisp: OLIVE.

入出力のための便利なパターンをいくつか見てみましょう。

ユーザー入力を求める: y-or-n-p, yes-or-no-p, read, read-line

y-or-n-p and yes-or-no-p 関数は prompt を表示し、処理を止めて “yes” または “no” の入力(短い形式または長い形式)を待ちます。

read 関数は、他の引数なしで呼び出されると処理を止めて入力を待ちますが、Lisp form を読み取ります。

CL-USER> (read)
|               ;; <---- point waiting for input.

(1+ 2) と入力すると、次の結果が見えます。

(1+ 2)

3 が表示されると思いましたか。

もう一度試してみましょう。

(read)
hello  ;; our input
HELLO  ;; return value: a symbol, hence capitalized

(read)
(list a b c)   ;; our input
(LIST A B C)   ;; return value: some s-expression.

結果は評価されていません

read 関数はソースコードを読み取ります。文字列ではありません。これは s-expression を返します。

文字列が欲しい場合は、引用符の中に書いてください(または次の節を参照してください)。

まだコードを評価してはいません。そのためには eval があります。

(read)
(1+ 2)  ;; input
(1+ 2)  ;; return value

(eval *)
;; 3

これが、最も原始的な REPL、つまり Read-Eval-Print-Loop を作る方法です。

CL-USER> (loop (print (eval (read))))
(1+ 1)  ;; input

2 (1+ 2)  ;; result + my next input

3

上の行は永遠に loop し、抜け出す方法はありません。ただし (quit) は別で、これは top-level REPL も終了します。次は、q symbol(文字列ではありません)を見たら終了する、とても単純な loop です。

(loop for input = (read)
      while (not (equal input 'q))
      do (print (eval input)))

入力を文字列として読む: read-line

入力を文字列として読むには、read-line を使います。

CL-USER> (defparameter *input* (read-line))
This is a longer input.
*INPUT*

CL-USER> *input*
"This is a longer input."

これは複数行を読みません。

プログラムの標準出力をリダイレクトする

次のようにします。

(let ((*standard-output* <some form generating a stream>))
  ...)

*standard-output* は dynamic variable なので、LET form の body の実行中にそれを参照するすべての箇所は、あなたが bind した stream を参照します。LET form を抜けると、通常実行であれ、関数全体を抜ける RETURN-FROM であれ、exception であれ、その他何であれ、*STANDARD-OUTPUT* の古い値が復元されます。(ちなみに、Common Lisp で global variable が他の言語ほど壊れたものにならないのはこのためです。特定の form の実行中だけ bind でき、その form が終わった後に以前の値を失う危険がないので、かなり安全に使えます。すべての関数に渡される追加の parameter のように振る舞います。)

プログラムの出力をファイルへ送るべき場合は、次のようにできます。

(with-open-file (*standard-output* "somefile.dat"
                                   :direction :output
                                   :if-exists :supersede)
  ...)

with-open-file はファイルを開き、必要なら作成し、*standard-output* を bind し、body を実行し、ファイルを閉じ、*standard-output* を以前の値へ復元します。これ以上快適にはなりません。

文字 stream による忠実な出力

ここでいう faithful output とは、0 から 255 の間の code を持つ文字がそのまま書き出されることを意味します。つまり、stream に対して (princ (code-char 0..255) s) でき、8-bit byte が書き出されると期待できるということです。Unicode と 16 bit または 32 bit の文字表現の時代には、これは自明ではありません。これは、ä、ß、þ といった文字の char-code が 0..255 の範囲にあることを要求するものではありません。実装は任意の code を使えます。しかし、とりわけ #\Newline から CRLF への変換が起きないことは要求します。

Common Lisp には、character I/O と byte(binary)I/O を区別する長い伝統があります。たとえば read-byteread-char は標準に含まれています。一部の実装では両方の関数を交換可能に呼べます。他の実装では、どちらか一方だけが許可されます。simple stream proposal は、両方が可能な bivalent stream の概念を定義しています。

さまざまな element-type が有用なのは、一部の protocol が channel 上で 8-Bit output を送信できることに依存しているからです。たとえば HTTP では、header は通常 ASCII であり、行終端として CRLF を使うべきです。一方で body は MIME type application/octet-stream を持つことができ、その場合 CRLF 変換はデータを破壊します。(これは、誤って設定された Webserver が未知のファイルを text/plain MIME type として宣言したとき、MS-Windows 上の Netscape browser が送信データを壊す仕組みです。text/plain はほとんどの Apache 設定でデフォルトです)。

以下は、実装依存の選択肢と振る舞い、および実験用のコードの一覧です。

SBCL

任意の byte を string にロードするには、:iso-8859-1 external format を使います。例:

(uiop:read-file-string "/path/to/file" :external-format :iso-8859-1)

CLISP

CLISP では、次を使うことで faithful output が可能です。

:external-format
(ext:make-encoding :charset 'charset:iso-8859-1
                   :line-terminator :unix)

(SETF (STREAM-ELEMENT-TYPE F) '(UNSIGNED-BYTE 8)) も使えます。ここで SETF できることは CLISP 固有の拡張です。:EXTERNAL-FORMAT :UNIX を使うと portability problem が発生します。MS-Windows 上のデフォルト文字セットは CHARSET:CP1252 だからです。CHARSET:CP1252 は、たとえば (CODE-CHAR #x81) の出力を許しません。

;*** - Character #\u0080 cannot be represented in the character set CHARSET:CP1252

code > 127 の文字は ASCII で表現できません。

;*** - Character #\u0080 cannot be represented in the character set CHARSET:ASCII

AllegroCL

#+(AND ALLEGRO UNIX) :DEFAULT(未テスト)- UNIX では十分なようですが、AllegroCL の MS-Windows port では動かないでしょう。

LispWorks

:EXTERNAL-FORMAT '(:LATIN-1 :EOL-STYLE :LF)(Marc Battyani により確認済み)

試すためのサンプルコードを示します。

(defvar *unicode-test-file* "faithtest-out.txt")

(defun generate-256 (&key (filename *unicode-test-file*)
			  #+CLISP (charset 'charset:iso-8859-1)
                          external-format)
  (let ((e (or external-format
	       #+CLISP (ext:make-encoding :charset charset
                           :line-terminator :unix))))
    (describe e)
    (with-open-file (f filename :direction :output
		     :external-format e)
      (write-sequence
        (loop with s = (make-string 256)
	      for i from 0 to 255
	      do (setf (char s i) (code-char i))
	      finally (return s))
       f)
      (file-position f))))

;(generate-256 :external-format :default)
;#+CLISP (generate-256 :external-format :unix)
;#+CLISP (generate-256 :external-format 'charset:ascii)
;(generate-256)

(defun check-256 (&optional (filename *unicode-test-file*))
  (with-open-file (f filename :direction :input
		     :element-type '(unsigned-byte 8))
    (loop for i from 0
	  for c = (read-byte f nil nil)
	  while c
	  unless (= c i)
	  do (format t "~&Position ~D found ~D(#x~X)." i c c)
	  when (and (= i 33) (= c 32))
	  do (let ((c (read-byte f)))
	       (format t "~&Resync back 1 byte ~D(#x~X) - cause CRLF?." c c) ))
    (file-length f)))

#| CLISP
(check-256 *unicode-test-file*)
(progn (generate-256 :external-format :unix) (check-256))
; uses UTF-8 -> 385 bytes

(progn (generate-256 :charset 'charset:iso-8859-1) (check-256))

(progn (generate-256 :external-format :default) (check-256))
; uses UTF-8 + CRLF(on MS-Windows) -> 387 bytes

(progn (generate-256 :external-format
  (ext:make-encoding :charset 'charset:iso-8859-1 :line-terminator :mac)) (check-256))
(progn (generate-256 :external-format
  (ext:make-encoding :charset 'charset:iso-8859-1 :line-terminator :dos)) (check-256))
|#

高速な bulk I/O

大量のデータをコピーする必要があり、source と destination がどちらも stream(同じ element type)である場合、read-sequencewrite-sequence を使うと非常に高速です。

(let ((buf (make-array 4096 :element-type (stream-element-type input-stream))))
  (loop for pos = (read-sequence buf input-stream)
        while (plusp pos)
        do (write-sequence buf output-stream :end pos)))

Page source: ja/io.md

T
O
C