イベント駆動型プログラムのエラー処理
イベント駆動型のプログラムでは、エラー処理の記述が面倒になることがある。これを何とかしたい。
例えば、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は使えなさそう。