文字をバッファへ戻す

プログラミング言語 C には ungetc という関数がある。 ストリームから読み込んだ文字を再びストリームに戻す機能を持った関数だ。 パーサを書く場合などには先読みが必要な場合があり、そういう場合に ungetc は利用できる。 getc などで読み込んだ一文字が不要だったならストリームに戻せばよい。

ungetc の挙動は JIS ではこう説明されている。

7.19.7.11 ungetc 関数

形式
 #include<stdio.h>
 int ungetc(int c, FILE *stream);
機能

ungetc 関数は、 c で指定される文字を (unsigned char 型に変換して) stream が指す入力ストリームに押し戻す。 その後の同一ストリームに対する読取りの際には、押し戻された文字を逆順で返す。 押し戻した文字を読み取る前に (同一ストリームに対する) ファイル位置付け関数 (fseek, fsetpos 又は rewind) の呼出しが成功すると、押し戻されていた文字を捨てる。 ungetc 関数は、ストリームに対応している外部記憶を変化させない。

どの処理系でも 1 文字の押戻しを保証する。 ストリームに対して読取り又はファイル位置付けを 1 回も間にはさまずに ungetc 関数を難解も繰り返し呼び出すと、失敗することがある。

c の値がマクロ EOF の値に等しいとき、操作は失敗し、入力ストリームは変化しない。

ungetc 関数の呼出しが成功すると、そのストリームに対するファイル終了表示子をクリアする。 すべての押し戻された文字を読み取るか捨てた後のストリームのファイル位置表示子の値は、押し戻される前と同じでなければならない。 ungetc 関数の呼出しが成功してからすべての押し戻された文字を読み取るか捨てるまでの間は、テキストストリームでのファイル位置表示子の値を未規定とする。 バイナリストリームの場合、 ungetc 関数の呼出しが成功するごとにファイル位置表示子を 1 ずつ減算するが、呼出し前の値が 0 のときには呼出し後の値は不定とする(246)。

返却値

ungetc 関数は、変換されて押し戻された文字を返す。 操作が失敗した場合、 EOF を返す。

JISX3010:2003 プログラミング言語C

では、 Scheme ではどうかというと異なる方法がとられている。 C では「ストリームから文字を読み込んでみて不要なら戻す」という体裁だが Scheme では「ポートを覗いてみて必要なら取り出す」という形だ。 ポートの状態を変化させずに次の文字が何か知る手続き peek-char が用意されているのである。

(peek-char) 手続き

(peek-char port) 手続き

テキスト入力 port から次に入手可能な文字を返すが、その次に来る文字を指すように port を更新することはしない。

それ以上文字が入手可能でない場合、 end-of-file オブジェクトが返される。

https://github.com/oitomo/r7rs-small-spec-ja

このふたつの違いに戸惑ったということに言及した記事を読んだ。

How To Become A Hacker: 言語処理系とは

違いを理解していたとしても、 C で書かれたプログラムを Scheme にベタ移植したいというときには C 方式で操作できると便利なこともあると思う。 入力ポートを覆って C 方式の操作を受付けるようにするライブラリを書いてみた。

(define-library (c-like-port)
  (export c-like-port-wrapper getc ungetc)
  (import (scheme base)
          (scheme case-lambda)
          (scheme read))
  (begin
    (define-record-type <c-like-port>
      (c-like-port port buffer)
      c-like-port?
      (port c-like-port-origin)
      (buffer c-like-port-buffer set!-c-like-port-buffer))

    (define (c-like-port-wrapper port)
      (c-like-port port '()))

    (define-syntax pop!
      (syntax-rules ()
        ((_ place)
         (let ((temp (car place)))
           (set! place (cdr place))
           temp))))

    (define-syntax push!
      (syntax-rules ()
        ((_ stack item)
         (set! stack (cons item stack)))))

    (define getc ; Alternative read-char
      (case-lambda
       ((clp)
        (let ((buffer (c-like-port-buffer clp)))
          (if (null? buffer)
              (read-char (c-like-port-origin clp))
              (begin
                (let ((t (pop! buffer)))
                  (set!-c-like-port-buffer clp buffer)
                  t)))))
       (()
        (getc (current-input-port)))))

    (define ungetc
      (case-lambda
       ((ch clp)
        (let ((buffer (c-like-port-buffer clp)))
          (push! buffer ch)
          (set!-c-like-port-buffer clp buffer)))
       ((ch)
        (ungetc ch (current-input-port)))))
    ))

以下のように使う。

(import (scheme base)
        (scheme char)
        (scheme write)
        (c-like-port))

(define (read-number in)
  (let loop ((acc '()))
    (let ((c (getc in)))
      (if (digit-value c)
          (loop (cons c acc))
          (begin
            (ungetc c in)
            (string->number (list->string (reverse acc))))))))

(let ((in (c-like-port-wrapper (open-input-string "12$"))))
  (write (read-number in))
  (newline)
  (write (getc in))
  (newline))

(let ((in (c-like-port-wrapper (open-input-string "12 "))))
  (write (read-number in))
  (newline)
  (write (getc in))
  (newline))

少し覆いを被せれば柔軟に使い勝手を変更することが出来るので、他言語から移植するときにはどうやって違いを埋めるか考えてみるのはよいと思う。

Document ID: 908cd087dc2523ef5698618bbdcb5687