Scheme で read

Scheme の入出力ライブラリ関数の中でも read は異彩を放っている。

read-char がテキストを一文字ずつ読み込むのに対し、read は S 式をひとつずつ読み込む。

以下は指定したファイルから S 式を読み込む関数。

(define extract-sexp
  (lambda (filename)
    (with-input-from-file filename
      (lambda ()
        (let loop ((buff '()) (s (read)))
          (if (eof-object? s)
              (reverse buff)
              (loop (cons s buff) (read))))))))

この関数で自分自身を読み込むと、read は関数全体を読み込む。

この read で指定したファイルから if 関数を抜き出すのが以下の関数。

(define extract-if
  (lambda (lst)
    (let ((buff '()))
      (if (null? lst)
          '()
          (let loop ((head (car lst)) (tail (cdr lst)))
            (if (and (not (null? head)) (list? head))
                (if (equal? (car head) 'if)
                    (begin
                      (set! buff (cons head buff))
                      (loop (car head) (cdr head))
                      (if (not (null? tail))
                          (loop (car tail) (cdr tail))))
                    
                    (loop (car head) (cdr head))))
            (if (null? tail)
                (reverse buff)
                (loop (car tail) (cdr tail))))))))

シンボルを外部から渡すことで、どんな関数にもできそうだ。内部の let に head と tail を分けて渡すというアイディアが出てくるまでに結構苦労した。

以下は抽出した if 式が正しいフォームかどうかをチェックする関数。

(define check-if
  (lambda (sexp)
    (cond
      ((null? sexp) #f)
      ((not (list? sexp)) #f)
      ((not (equal? (car sexp) 'if)) #f)
      ((null? (cdr sexp)) #f)
      ((null? (cadr sexp)) #f)
      ((not (or (= (length (cddr sexp)) 1) (= (length (cddr sexp)) 2))) #f)
      (else #t))))

本当は一致する条件をネストさせていって、その場合だけ #t、それ以外は #f としたかったんだけど、ネストが深くなると何がなんだかわからなくなるので、Haskell のパターンマッチみたいな作りになってしまった。

read を使えば S 式がデータとして取得できて、そのデータはコードであって、SchemeScheme の処理系を作るのは(Scheme に精通していれば)割と簡単にできそうだと思った。