あどけない話

インターネットに関する技術的な話など

Haskellでtry/throw/catch/finally

Haskellで実装する関数の多くでは、いわゆる Java の try/throw/catch/finally は必要になりません。しかし、IO が絡んでくると話は別です。異常な状態に陥ったら、throw してトップレベルに戻る方が、コードがすっきり書ける場合があります。

Java で言う try/catch

Haskellを勉強していると、すぐにcatchが見付かります。catch はプレリュードで定義されていて、以下のような型を持っています。

catch :: IO a -> (IOError -> IO a) -> IO a

第一引数のアクションの中で例外が生じると、第二引数の関数が補足します。つまり、第二引数が Java でいう catch ブロックにあたります。こういう風に使います。

catch 何らかのアクション (\e -> 何らかの例外処理)

昔 Monadius を読んだときに catch が二項演算子として使ってあって感動しました。

何らかのアクション `catch` (\e -> 何らかの例外処理)

一方、Real World Haskell を読むと、長い引数は最後に置く技法が載っています。

foo = do
    flip catch handler $ do 
      何らかのアクション
      何らかのアクション
  where
    handler e = 何らかの例外処理

Java で言うthrow

僕は長い間、Java の throw に当たる関数が何なのか分かりませんでした。ちょっと前に、Haskell 98 の仕様書を眺めていて、ようやく分かりました。

userError :: String -> IOError
ioError :: IOError -> IO a

つまり、ある文字列を引数にして例外を発生させるには、ioError.userError を呼べばよいのです。

さらに、GHC に付いてくるライブラリを読んでいて、fail が ioError.userError と定義してあることを知り、驚きました。IO もモナドですから、fail なんですね。だから、文字列を引数にして例外を起こすには、単に fail を呼べばいいだけです。

fail "network error"

外側に catch があれば、そこまで一気に戻れます。

Java で言う finally

Java に finally があるのは、try のブロックから return/break/continue で抜け出せるので、確実に実行したい部分を明記する必要があるからです。Haskell にはそういう制御構造はありませんから、catch の後に書いたコードは必ず実行されます。

という訳で、全体像はこんな感じになります。

foo = do
    flip catch handler $ do 
      何らかのアクション
      when エラー (fail "文字列")
      何らかのアクション
    確実に実行したいアクション
    確実に実行したいアクション
  where
    handler e = 何らかの例外処理

おまけ

興味がある人は、Haskell の try や bracket を調べてみましょう。