たごもりすメモ

コードとかその他の話とか。

Rubyで try-with-resources (あるいは with/using)をやりたいという話

前のエントリで書いたうちに入っていたが、Rubyでも try-with-resources (あるいはPythonで言う with やC#で言う using)が使いたいなー、という話。

Feature #13923: Idiom to release resources safely, with less indentations - Ruby trunk - Ruby Issue Tracking System

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ので、実装するなら自分が書きたいように書けるようにしてみた。

github.com

これで上記の例は以下のように書ける。

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を使ってあるブロック内に存在する記述だけを正確に追いかけることが現状不可能なことによる。変な書き方したら動作がおかしくなるかも。

ということで

作りました。こんなんどうですかね、という提案に近いですが。

*1:golangのdeferはその関数の終わりで自動的に解放されてインデントも増えないのに対して、Rubyで自分で実装する方向だとブロックをひとつ作らないといけなくてgolangみたいに簡潔な記述にならないのが気にくわない

*2:extendで動的に足している、まじべんり