エラーモナド in Ruby
PHPの@演算子っぽいものをいじってたら Ruby でエラーモナドっぽいものができました。
こんな感じで使えます。
result = try { parseint("hoge") }. catch(ArgumentError){ -1 }. catch{|e| puts e; -2 } res_value = result.value if result.good?
begin/rescue 書くのと見た目ほとんど変わりません。
最後まで拾われなかった例外は、value を呼んだ時に投げられます。
エラーハンドラをたくさん用意しておいて、順番に試して最初に例外吐かなかったものを採用、とか。
maybe_error = try { ... } result = handlers.inject(maybe_error) {|m,(e,h)| m.catch(e,&h)}.value
コードはこんな感じ。(ちょっと書き足した)
def try(exns=[]) begin r = yield(*exns) Either.new(true, r) rescue => e exns.unshift(e) Either.new(false, exns) end end class Either def initialize(g, v) @good, @value = g, v end def catch(*exns, &b) exns = [Exception] if exns.empty? lastexn = @value.first unless @good if !@good && exns.any?{|e| lastexn.kind_of? e} try(@value, &b) else self end end def value if @good @value else raise @value.first end end def good? @good end end
catch節での例外発生について
どういうときに普通のbegin/rescue式と違ってくるかというと
try { A }.catch(e1,e2){ B }.catch(e3){|x,y| C }
とあったときに、Aで例外が起こって、Bでそれを回復しようとしたところで別の例外が起こったとすると、C からはどの例外が見えるか。
普通のbegin/rescue式だと A の例外しか補足できない。Bの中で起こる例外に対処したい場合はBブロックの中で別のbegin/rescue式を書く必要がある。
それに対し上の実装では、Bの例外もCに流れるようになっている。
上のケースで C に処理が移るのは A で発生した例外 e1 or e2 が B で catch され、そこで例外 e3 が投げられた場合と、Aでe3が投げられて最初のcatchでe1,e2のどちらにもマッチせずBがスルーされた場合の2通りのパスがある。(前者の場合には x,y にそれぞれ Bの例外,Aの例外が入っているし、後者の場合には x に A の例外が入っており y は nil になる。)
両方の場合で C での対処方法が同じようなものになる場合やA,Bのどちらか一方でしかe3にマッチする例外が発生しない場合、線形に書けてすっきりするし、そうでない場合はややこしくなる。
begin/rescue式と比べてどちらの方がきれいに書けるかは場合によるが、多くの場合こっちで十分だと思える。