Guile では call-with-input-file の proc 内でエラーが起きるとポートが閉じられない件

問題

あんまり問題とは言えないかもしれないが、 (proc port) 内でエラーが起きた場合、ポートを閉じる手続きが呼ばれない。
助けてドラえもん!!

とはいえ、プログラムが終了する時に全てのポートは自動的に閉じられるとかどこかに書いてあった気がするので、 そんなに問題にはならないかもしれないんだけど、Ruby で言うような File.open :

# ものすごく大雑把な File.open() の実装:
class File
  def open(path, mode = "r", perm = 066)
    f = File.new( path, mode, perm )
    yield f
  ensure
    f.close
    # 何らかのエラーが起こったとしても、ensure 節で
    # 絶対に閉じられるため、安心。
  end
end

と同じようなことをしたくなった時に困るので、考えてみたいと思う。

検証

Gauche においての call-with-input-file

試しに Gauchecall-with-input-file を見てみる:

(define-in-module scheme (call-with-input-file filename proc . flags)
  (let1 port (apply open-input-file filename flags)
    (unwind-protect (proc port)
      (when port (close-input-port port)))))

これは:

(define (call-with-input-file filename proc . flags)
  (let1 port (apply open-input-file filename flags)
    (unwind-protect (proc port)
      (when port (close-input-port port)))))

と一緒。

unwind-protect というなんかすごそうなシンタックス?で守られてる。 でも Guile にはそんなものはない。

Guile においての call-with-input-file

Guile においての call-with-input-file は 2 つある。 1 つ目は R5RS の call-with-input-file だけど:

(define* (call-with-input-file
          file proc #:key (binary #f) (encoding #f) (guess-encoding #f))
  "PROC should be a procedure of one argument, and FILE should be a
string naming a file.  The file must
already exist. These procedures call PROC
with one argument: the port obtained by opening the named file for
input or output.  If the file cannot be opened, an error is
signalled.  If the procedure returns, then the port is closed
automatically and the values yielded by the procedure are returned.
If the procedure does not return, then the port will not be closed
automatically unless it is possible to prove that the port will
never again be used for a read or write operation."
  (let ((p (open-input-file file
                            #:binary binary
                            #:encoding encoding
                            #:guess-encoding guess-encoding)))
    (call-with-values
      (lambda () (proc p))
      (lambda vals
        (close-input-port p)
        (apply values vals)))))

2 つ目は R6RScall-with-input-file で:

(define (call-with-input-file filename proc)
  (call-with-port (open-file-input-port filename) proc))

便利な call-with-port が付いて………:

(define (call-with-port port proc)
  "Call @var{proc}, passing it @var{port} and closing @var{port} upon exit of
@var{proc}.  Return the return values of @var{proc}."
  (call-with-values
      (lambda () (proc port))
    (lambda vals
      (close-port port)
      (apply values vals))))

やっぱ、ダメじゃねぇか!!!
やっぱり call-with-values 使ってんじゃねーかァァァァァァァァァァァァァァ!!!!!!!!!!!!!!!

マジでそうなの?

Guile で call-with-values を使ってるけど、本当に後の処理(ポートを閉じる処理)が呼ばれないのか検証してみた。

(define error-happen? #t)

(call-with-values
    (lambda ()
      (when error-happen?
      ;;;
      ;;; producer で起こったエラーはそのまま突き抜けてしまう。
      ;;; 
        (error "*Oops!* teleporter"))
      (values 4 5))
  (lambda (a b)
    (display "handler")
    (newline)))

上記のコードを demo-call-with-values.scm に保存して実行してみた結果が以下である:

% guile ./demo-call-with-values.scm
Backtrace:
In ice-9/boot-9.scm:
 160: 7 [catch #t #<catch-closure 1cac460> ...]
In unknown file:
   ?: 6 [apply-smob/1 #<catch-closure 1cac460>]
In ice-9/boot-9.scm:
  66: 5 [call-with-prompt prompt0 ...]
In ice-9/eval.scm:
 432: 4 [eval # #]
In ice-9/boot-9.scm:
2404: 3 [save-module-excursion #<procedure 1cce9c0 at ice-9/boot-9.scm:4051:3 ()>]
4058: 2 [#<procedure 1cce9c0 at ice-9/boot-9.scm:4051:3 ()>]
In /home/rihine/workspace/guile-cwv/./demo-call-with-values.scm:
  10: 1 [#<procedure 1d9ebc0 ()>]
In unknown file:
   ?: 0 [scm-error misc-error #f "~A" ("*Oops!* teleporter") #f]

ERROR: In procedure scm-error:
ERROR: *Oops!* teleporter

上記の通り、 handler とは出力されていない(そりゃそーだ)。 替わりにテレポーターに引っかかってしまった。

解決法みたいなもの

すごく簡単な解決方法として、 Guile で unwind-protect を実装するというものが考えられる。
あるいは、 finally のようなものを継続を使って実装する、とか。

そういえば、 catch の後にポートを閉じれば同じようなことができるはず。