イベント駆動型プログラムのエラー処理

イベント駆動型のプログラムでは、エラー処理の記述が面倒になることがある。これを何とかしたい。


例えば、keyからidを引き、idからdata引くプログラムを書きたいとする。
手続き型で書くと以下のようになる:

def doit(key)
  id = get_id(key)
  data = get_data(id)
  return data
end

ここで、get_id と get_data は長くブロックするので、イベント駆動型にしたいとする。

そうするとプログラムの正常系の処理は↓このようになる。

def doit(key, &callback)
  get_id(key) {|id|
    get_data(id) {|data|
      callback.call(data)
    }
  }
  # futureを返すのは良いアイディア
end

ここまでが前提条件。
このプログラムのエラー処理を愚直に書くと、↓このように非常に冗長になってしまうのを何とかしたい:

def doit(key, error_callback, &result_callback)
  begin
    get_id(key) {|id,error|
      if error
        error_callback.call("error!")
      else
        begin
          get_data(id) {|data,error|
            if error
              error_callback.call("error!")
            else
              result_callback.call(data)
            end
          }
        rescue
          error_callback.call("error!")
        end
      end
    }
  rescue
    error_callback.call("error!")
  end
end

方法1:Fiberを使う

実装が複雑になるので、とりあえず使わないことにする。

方法2:エラーハンドラを伝播させていく

get_id や get_data にエラーハンドラを引数として渡せるようにする。get_id や get_data は決して例外を投げず、エラーが起こった場合にはエラーハンドラを呼ぶことにする。

def doit(key, error_callback, &result_callback)
  get_id(key, error_callback) {|id|
    get_data(id, error_callback) {|data|
      result_callback.call(data)
    }
  }
end

get_idの実装は↓こんな感じになる。

def get_id(key, error_callback, &result_callback)
  @rpcClient.callback(:get_id, key) {|result, error|
    if error
      error_callback.call(error)
    else
      id = result
      result_callback.call(id)
    end
  }
end

これでだいたい良いのだが、エラーの返し方をエラーの種類によって変えたかったりする。
例えば、get_idは成功したがget_dataが失敗した場合は、エラーを返すのではなくnilを返すようにしたい。そういう場合には↓こう書く。

def doit(key, error_callback, &result_callback)
  error_callback_nil = Proc.new {
    result_callback.call(nil)
  }
  get_id(key, error_callback) {|id|
    get_data(id, error_callback_nil) {|data|
      result_callback.call(data)
    }
  }
end

方法3?

方法2は、引数にいちいちエラーハンドラが出現するのがわずらわしい。もっとうまく書けないだろうか。
スレッドは使い回すので、TLSは使えなさそう。