エラーモナド 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式と比べてどちらの方がきれいに書けるかは場合によるが、多くの場合こっちで十分だと思える。