前のエントリで書いたうちに入っていたが、Rubyでも try-with-resources (あるいはPythonで言う with やC#で言う using)が使いたいなー、という話。
Ruby本体にFeature request出してみたんだけど、ライブラリでやればいいんじゃないの、という結論になってしまって、ええーと思っている。ライブラリだといくらでもある方法のひとつになってしまって、他のプログラマ(他所のライブラリ作者とか)に「これを使え」という話がしづらいんだよねえ。あとgemにして広く使われたとしても、この機能のバージョン指定がgemどうしで衝突してトラブルとか、死んでも死にきれない感があるし。
どうせやりかたの選択肢がほとんどないんだから、こういうものは言語側で方法を提供してほしい、と思ってはいる。
どういう用途でこれが必要なのかというと複数のリソースを確保しつつ確実に(逆順で)解放してほしいというケースで、最近思い付いた良い例としては (1) データベースへの接続を作る (2) その接続を用いて PreparedStatement を作る、みたいなものが当たる。
begin conn = Database.new(addr, port, dbname) begin stmt = conn.prepare(sql) # stmt.execute(...) ensure stmt.close end ensure conn.close end
Rubyであればopenとブロックを使ってこのリソース確保と解放をする例が多いと思うが、これはリソースひとつにつきインデントがひとつ深くなるので簡単にネストの深いコードになり、全体として簡潔とは言いがたい見た目になる。
Database.new(addr, port, dbname) do |conn| conn.prepare(sql) do |stmt| # stmt.execute(...) end end
またこういったブロックを渡せるメソッドがそれぞれのクラスで実装されている必要がある、が、現実としてそうなっていないケースは多い。例えば上記のコード例では conn.prepare にブロックを渡せる必要があるが、mysql2 の prepared statement を作るためのメソッドには実際にはブロック渡しはできない。
ところでライブラリとして実装してみる
とはいえ、とりあえずライブラリとしてでも実装がないと話にならない、みたいな面もあるのかなと思ったので、自分でエイヤと実装してみた。bugsのやりとりではdeferでやるという方向に倒れかけてたけど、自分の好みじゃない*1ので、実装するなら自分が書きたいように書けるようにしてみた。
これで上記の例は以下のように書ける。
require "with_resources/toplevel" using WithResources::TopLevel # once per file with(->(){ conn = Database.new(addr, port, dbname); stmt = conn.prepare(sql) }) do |conn, stmt| # stmt.execute(...) end
簡潔! 何よりネストが深くならない!
見た目としてはJavaのtry-with-resourcesに似ていて、動作もそっくりそのまま……のはず。リソース解放時などに複数の例外が起きたときはふたつめ以降の例外は `e.suppressed` で参照できる配列*2に格納されるようになっているのも try-with-resources のまま。
キモとしては with に渡した lambda に複数の文が書け、そこのローカル変数にセットした値が後のブロックにも渡ってくるというところ。これはキモい。
TracePointとbindingを使ってあれこれやった。ちょいマジカルで、かつ実装としてはちょっと不安定だと思う。リソース解放用のブロックに存在するローカル変数だけをキャプチャしたいんだけど、TracePointを使ってあるブロック内に存在する記述だけを正確に追いかけることが現状不可能なことによる。変な書き方したら動作がおかしくなるかも。
ということで
作りました。こんなんどうですかね、という提案に近いですが。