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
試しに Gauche の call-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 つ目は R6RS の call-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
の後にポートを閉じれば同じようなことができるはず。