Clojureの標準にあるwith-open関数(マクロ)にはちょっとした問題があります。
本体の処理とクローズ処理の両方で例外が発生した場合、クローズ処理で発生した例外が伝播してしまいます。
with-open-sample.clj
(with-open [c (reify java.lang.AutoCloseable
(close [this] (throw (java.io.IOException. "closeA"))))]
(throw (IllegalStateException. "tryB")))
java.io.IOException: closeA
(Unknown Source) user/eval7216[fn]
(Unknown Source) user/eval7216
...
Java 7で導入されたtry-with-resources構文ではそのへんのことが考慮されていて、クローズ処理で発生した例外は本体の例外のSuppressed領域に入るようになっています。
例えば、以下のコードを実行すると
TrySample.java
package trysample;
public class TrySample {
public static void main(String[] args) throws Exception{
try(AutoCloseable a = () -> {throw new java.io.IOException("closeA");}){
throw new IllegalStateException("tryB");
}
}
}
このような結果になります。
Exception in thread "main" java.lang.IllegalStateException: tryB
at trysample.TrySample.main(TrySample.java:6)
Suppressed: java.io.IOException: closeA
at trysample.TrySample.lambda$main$0(TrySample.java:5)
at trysample.TrySample$$Lambda$1/2536472.close(Unknown Source)
at trysample.TrySample.main(TrySample.java:7)
Java 7ではこれを実現するためにThrowableクラスにaddSuppressedメソッド、getSuppressedメソッドなどが追加されています。
これを利用してJava 7対応版のwith-openとしてauto-closeマクロを作ってみます。
auto-close.clj
(defmacro try-finally [main-form dispose-form]
`(with-local-vars [th# nil]
(try ~main-form
(catch Throwable t# (do (var-set th# t#) (throw t#)))
(finally
(try ~dispose-form
(catch Throwable t#
(if (nil? @th#) (throw t#) (do (.addSuppressed ^Throwable @th# t#) (throw @th#)))))))))
(defmacro auto-close [bindings & body]
{:pre[(vector? bindings)
(even? (count bindings))
(every? symbol? (take-nth 2 bindings))]}
(if (zero? (count bindings)) `(do ~@body)
`(let ~(subvec bindings 0 2)
(try-finally (auto-close ~(subvec bindings 2) ~@body)
(.close ~(bindings 0))))))
これでちゃんと本体の例外が返るようになりました。
auto-close-sample.clj
(auto-close [c (reify java.lang.AutoCloseable
(close [this] (throw (java.io.IOException. "closeA"))))]
(throw (IllegalStateException. "tryB")))
java.lang.IllegalStateException: tryB
(Unknown Source) user/eval7222
...
※副作用があるコードの場合、with-local-varsとvar-setの組み合わせを使うと便利ですね~