初めての人のためのLISP - 第14講 入出力がなければプログラムは生きていけぬ

初めての人のためのLISP[増補改訂版]のメモです。
最初:初めての人のためのLISP (第1講-第5講) - もうカツ丼でいいよな

データを受け取って結果を出力する

(write (+ (read) (read))

上の式をインタプリタで評価すると待機状態になり、入力がreadに代入され評価された結果が出力される。

CL-USER> (write (+ (read) (read)))
12345 67890 ;; <= キーボードからの入力
80235       ;; <= 出力
80235       ;; <= 戻り値

read

  • readは「文字の列」をS式に変換して返す関数
  • 先頭から文字を一つずつ取り出せる論理構造をもつものをストリーム(stream)と呼ぶ
    • ファイル、キーボードからの入力、通信回路からの情報...etc.
  • 引数としてストリームを指定できるが、随意変数なので省略できる

随意変数

  • &optionalに続いて指定される引数
  • 省略可能で、省略した場合は指定したデフォルト値に束縛される
  • デフォルト値は引数と並べてリストにする
  • デフォルト値はアトムでなくともよい
  • デフォルト値を指定しないとnilがデフォルト値に設定される
(defun foo (a &optional (b 5) c (d (* a b)))
       (list a b c d))
(foo 10) ; => (10 5 NIL 50)
(foo 1 2 3 4) ; => (1 2 3 4)
  • &restに続く任意個の引数は余剰変数などと呼ぶ
  • ラムダ式の引数は必須変数、随意変数、余剰変数の順に並ぶ

随意変数は指定する順番が合っている必要があり、スキップして途中のひとつだけ指定することもできない。後に出るキーワード変数がこれを可能にする。

パッケージ

  • Common Lispでは名前空間のことをシンボルパッケージまたはパッケージと呼ぶ
  • シンボルはいずれかのパッケージに属す
  • パッケージを明示してシンボルを指定するには"パッケージ名:シンボル名"の形式を使う(e.g. common-lisp:car)
  • Common Lisp標準のパッケージはlisp user keyword systemの4つ
    • lisp: carやcdrなど基本的なシンボルを含む
    • user: ユーザが定義したシンボルを含む
    • system: Lisp処理系内部で使用されユーザには公開されないシンボルを含む
    • keyword

keyword

  • keyword:は:だけに省略できる
  • キーワードを評価すると常にキーワード自身が返る
keyword:direction ; => :DIRECTION
:direction        ; => :DIRECTION

keyword引数

  • キーワード引数は&keyに続いて指定する
(defun foo (&key (a 1) (b 2) (c 3))
       (list a b c))
  • 関数を呼ぶとき、キーワードと並べて値を指定するとキーワードに対応する変数にだけ値を設定できる
  • 順番は無視できる
(foo :b 5) ; => (1 5 3)
(foo :c 12 :a nil) ; => (NIL 2 12)
  • キーワード変数の宣言は余剰変数よりも後

ストリーム

read関数は随意変数としてstreamを持っている。初期値は*standard-input*で、この変数には標準入力ストリームが入っている。

CL-USER> *standard-input*
#<SWANK-BACKEND::SLIME-INPUT-STREAM {10034A27E1}>

変数についているアステリスクは変数が非局所的な意味を持つ場合に慣習的に付けられている。

open close

ファイルに対応したストリームを作成するにはopenを使う

(open "hoge.xo" :direction :input)

これはファイルhoge.xoに対応するストリームを作成する。
キーワード引数:directionにはキーワードである:inputが引数として与えられている。キーワードを引数などに使用すればどのパッケージの変数であるか悩む必要がないため、目印としてよく使われる。
使用後はcloseを使ってストリームを閉じる。

(setq s (open "hoge.xo")) ; (direction :input)はデフォルト
(read s)
...
(close s)

read-byte read-char

read-byteはストリームから1バイトずつ読み込む。
read-charはストリームから1文字ずつ読み込む。

CL-USER> (read-char)
a
#\a

character

#\に文字が続くとき、全体で文字(character)を表す。文字は文字列とは異なる基本データ型である。

空白文字(whitespace)

#\space #\newline #\return #\tab

その他のread関数

  • peek-char: ストリームの先頭の1文字を見るが取り出さない
  • read-char-no-hang: ストリームから1文字とり出すがストリームが空でも待たない
    • ストリームがキーボードからの入力なら即nilが返る
  • unread-char: 指定した文字をストリームの先頭に戻す

簡略版read関数rd

;; 空白文字のセットを定義
(setq whitespace '(#\space #\newline #\return #\tab))
;; rd関数本体
(defun rd (&optional (stream *standard-input*)) ; 引数はストリーム
    (let ((char (read-vchar stream)) ; 最初の印字文字
      (cond ((eq char #\() ; 括弧の開始のとき
             (cond ((eq (peek-char t stream) #\)) ; すぐに閉じ括弧
                    (read-char stream) ; 閉じ括弧取り出し
                    nil ) ; ()はnil
                   (t (cons (rd stream) ; 括弧に続く最初のS式に
                            (rd-cdr stream))) )) ; ドット対のcdrやリストの続きをcons
            ((eq char #\)) ; 先頭の閉じ括弧はエラー
             (error 'syntax-error char) )
            ((eq char #\") (rd-string stream)) ; 文字列
            ((and (eq char #\#) ; #に続き空白文字を挟まず\がある
                  (eq (peek-char nil stream) #\\) )
             (read-char stream) ; \を取り除く
             (read-char stream) ) ; #\に続く1文字を文字として取り出す
            (t (rd-num-sym char stream)) ))) ; 数orシンボル
;; ドット対のcdr、リストの終わりの閉じ括弧、リストの続きを読み込む
(defun rd-cdr (stream)
    (let ((char (read-vchar stream))) ; 最初の印字文字
      (cond ((eq char #\.) ; ドット記法
             (prog1 (rd stream) ; ドットに続くS式
               (if (not (eq (read-vchar stream #\))) ; S式に閉じ括弧が続いていればok
                   (error 'syntax-error char) )))
            ((eq char #\)) nil) ; 閉じ括弧=リストの終了
            (t (unread-char char stream) ; .でも)でもない(=リストの要素)なら文字をもどして
               (cons (rd stream) ; 直近のS式と
                     (rd-cdr stream))) ))) ; 続くS式(or閉じ括弧)をcons
;; 最初の印字文字を読み込む
(defun read-vchar (stream)
       (peek-char t stream)
       (read-char stream))
;; 文字列連結
(defun string-append (&rest s)
       (apply #'concatenate (cons 'string s)))
;; 文字列読み込み
(defun rd-string (stream)
       (let (char chars)
            (loop (setq char (read-char stream))
                  (:until (eq char #\") ; ダブルクオートが出現したら
                          ; 文字を連結して文字列を作って終了
                          (apply #'string-append (nreverse chars)))
                  (setq chars (cons char chars)) ))) ; 文字をリストに溜める
;; 数orシンボルの読み込み
(defun rd-num-sym (char stream) ; charはrdで既に読み込んでいる1文字
    (let (chars token)
      (setq chars (list char))
      (loop (setq char (read-char stream))
            (:until (member char whitespace)) ; 空白か
            (:until (member char '(#\( #\))) ; 括弧で要素の区切り
                    (unread-char char stream)) ; 括弧の時は括弧を戻す
            (setq chars (cons char chars)) )
      (setq token (apply #'string-append (nreverse chars)))
      (cond ((parse-number token)) ; 数なら数に変換
            ((string= token "nil") nil) ; "nil"ならnil
            ((find-symbol token)) ; シンボルが既登録ならそのシンボル
            (t (intern token)) ))) ; シンボルの新規登録
;; 文字列が数なら数に変換して返す
(defun parse-number (token)
    (let (minus-flag char (result 0))
      ; 符号確認
      (cond ((eq (char token 0) #\+)
             (setq token (subseq token 1)) ) ; 符号部を飛ばす
            ((eq (char token 0) #\-)
             (setq minus-flag t)
             (setq token (subseq token 1)) ))
      (cond ((= (length token) 0) nil)
            (t (loop
                (setq char (char token 0))
                (:while (digit-char-p char) nil) ; charが数字ではなかったらnilを返す
                (setq result
                      (+ (digit-char-p char) (* result 10)))
                (setq token (subseq token 1))
                (:until (= (length token) 0) ; tokenが空になったら符号処理
                        (if minus-flag (- 0 result) result) ))))))

write

openの:directionに:outputを指定すると出力ストリームを開く。
出力ストリームに対してはwrite関数が使える。

(write "hoge")
"hoge" ; 出力
"hoge" ; 戻り値

readと同様write-byte、write-charも用意されている。

write簡易版wr

;; スペース、改行を出力
(defun space (&optional (stream *standard-output*))
       (write-char #\space stream))
(defun newline (&optional (stream *standard-output*))
       (write-char #\newline stream))
;;wr本体
(defun wr (x &optional (stream *standard-output*))
       (prog1
        x
        (cond ((null x) (wr-string "nil" stream)) ; 以下単純な場合分け
              ((characterp x) (wr-char x stream))
              ((numberp x) (wr-number x stream))
              ((stringp x)
               (write-char #\" stream)
               (wr-string x stream)
               (write-char #\" stream) )
              ((symbolp x)
               (if (keywordp x) (write-char #\: stream))
               (wr-string (symbol-name x) stream) )
              ((consp x)        ; コンスセル
               (write-char #\( stream) ; (から開始
               (loop (wr (car x) stream) ; carを書く
                     (:while (cdr x) (write char #\) stream) ) ; cdrが無くなったら)書いて終了
                     (space stream) ; 要素の直後はスペース1個
                     (:until (atom (cdr x)) ; cdrがアトム=ドット対
                         (wr-string ". " stream) ; ドット
                         (wr (cdr x) stream) ; cdr
                         (write-char #\) stream) ) ; )書いて終了
                     (setq x (cdr x)) ))))) ; carは書いたので取り除いてloop先頭へ
;; stringの出力
(defun wr-string (s stream)
  (loop (:until (= (length s) 0))
        (write-char (char s 0) stream) ; (char s 0)は1文字目
        (setq s (subseq s 1)) )) ; (subseq s 1)は2文字目以降の部分列
;; numberの出力
(defun wr-number (n stream)
  (cond ((< n 0) ; 負数の処理
         (write-char #\- stream) ; -を出力して
         (setq n (- 0 n)) )) ; 正数にしてしまう
  (let (chars)
       (loop ; 1の位からリストの前の方に付けていく
        (setq chars (cons (digit-char (mod n 10)) chars))
        (:until (< n 10))
        (setq n (/ n 10)) )
       (loop (:while chars) (write-char (pop chars) stream)) )) ; リストの先頭(最上位)から順次出力
;; characterの出力
(defun wr-char (c stream)
  (write-char #\# stream) ; #\を出力
  (write-char #\\ stream)
  (case c ; whitespaceの場合その種類で場合分け
    (#\space (wr-string "space" stream))
    (#\newline (wr-string "newline" stream))
    (#\return (wr-string "return" stream))
    (#\tab (wr-string "tab" stream))
    (t (write-char c stream)) )) ; whitespaceでなければ文字をそのまま書く

case

caseマクロは次の構造を持つ

(case(鍵穴1 式11 ...)
         (鍵穴2 式21 ...)
         ...
         (鍵穴n 式n1 ...))

鍵の値が鍵穴1..nの値とeq(eql)で順次比較されていき、tになった時点でその鍵穴に続くS式が順に評価され、最後の値が返る。
次のcond式とほぼ同様の意味。

(cond ((eq'鍵穴1) 式11 ...)
      ((eq'鍵穴2) 式21 ...)
      ...
      ((eq'鍵穴n) 式n1 ...))

宿題

B子: caseはcondを使ってマクロで簡単に書けそうですね。
T: 確かに。いい練習問題だから挑戦してみてください。ついでにさっきのloopも書いてみましょう(読者への宿題)。

case
(defmacro mycase (key &rest cases)
  (cons 'cond
        (expand-case key cases) ))
(defun expand-case (key cases)
  (mapcar (lambda (c) (cons (list 'eq key (car c)) (cdr c))) cases) )
loop

分からなかった…。またいつかやる。