Haskellの静的な型検査は強力ですが,プログラムの実行時に起こり得るすべての問題を解決できるわけではありません。例えば第5回で指摘したように,依存型を持たない現在の標準Haskellでは,検証できる問題の範囲に限界があります。また,FFIを使って呼び出す外部関数には,Haskellの型検査は行き届きません。実行前に静的に解決できない問題は,実行時に動的に解決する必要があります。このための手段を提供するのが例外処理です。

 例外処理については,これまで何度か断片的に説明しましたが,全体像をまとめて解説したことはありませんでした。次回以降にFFIと例外処理との関係について説明する前に,今回はHaskellの例外処理についてきちんと説明しておきましょう。

 例外処理の仕組みは,GHC 6.10に収録されるbase 4.0パッケージ以降で大きく変更されます。baseパッケージはデファクト・スタンダードのライブラリ実装の一つです。Hugsを含め今後リリースされるHaskell処理系では,この例外処理が広く使われることになるでしょう。今回は,この新しい例外処理の仕組みに基づいて説明します。

例外処理の基礎

 まず,どのようなときに例外が生じるかを見ていきましょう。

 例外が生じるのは,エラーになるような値を評価したときです。例えば,errorや,未定義の式を表すundefinedといった「エラーを直接引き起こす関数」を評価した場合に例外が生じます。

Prelude> error "error occur"
*** Exception: error occur
Prelude> undefined
*** Exception: Prelude.undefined

 他の言語でもおなじみの,ゼロ除算や桁あふれによる例外もあります。

Prelude> 12 `div` 0
*** Exception: divide by zero
Prelude> (minBound::Int) `div` (-1)
*** Exception: arithmetic overflow

 値の評価によって発生した例外を捕捉し,例外を発生させた式の代替になる処理を行うために用意されているのが,Control.Exceptionモジュールのcatch関数です。

Prelude Control.Exception> :t Control.Exception.catch
Control.Exception.catch :: (Exception e) =>
                           IO a -> (e -> IO a) -> IO a

 第12回で説明したように,Control.Exceptionモジュールのcatch関数は,Haskell98で定義されているPreludeの同名の関数と衝突してしまいます(参考リンク)。

Prelude Control.Exception> :t catch

<interactive>:1:0:
    Ambiguous occurrence `catch'
    It could refer to either `Prelude.catch', imported from Prelude
                          or `Control.Exception.catch', imported from Control.Exception

 使用の際には,モジュールで修飾された名前を使うか,Preludeのcatch関数を隠すことで,名前の衝突を防ぐ必要があります。

 catch関数を使ってみましょう。

module Exception where
import Control.Exception
import Prelude hiding (catch)

catchError   = catch (error "error occur") (\e -> return (e::SomeException) >> print "Caught error.")
catchError'  = catch (undefined) (\e -> return (e::SomeException) >> print "Caught error.")

*Exception> catchError
"Caught error."
*Exception> catchError'
"Caught error."

 例外の代わりに,例外を捕捉したことを示すメッセージが表示されているのがわかりますね。

 プログラムを詳しく見ていきましょう。

 例外処理に使われている「(\e -> return (e::SomeException) >> print "Caught error.")」という定義をまだるっこしく感じるかもしれません。なぜこのような定義になっているのでしょうか?

 理由は,catch関数の型が「(Exception e) => IO a -> (e -> IO a) -> IO a」になっているところにあります。catch関数は特定の例外を定義した型ではなく,Exceptionクラスのインスタンスである「不特定多数の例外を表現する型」を使って例外処理を行います。このため,何らかの方法で例外処理に使用する型を確定しなければなりません。しかし,Haskell98では「(\(e::SomeException) -> print "Caught error.")」のようにパターンに対して型シグネチャを書くことは許されません。

*Exception> :r
[1 of 1] Compiling Exception        ( Exception.hs, interpreted )

Exception.hs:8:36:
    Illegal signature in pattern: SomeException
        Use -XScopedTypeVariables to permit it
Failed, modules loaded: none.

 そこでeの型を確定させるために,「return (e::SomeException) >>」という余分な記述が必要になるのです。

 エラー・メッセージからわかるように,GHCでは-X*オプションやLANGUAGE指示文を使ってScopedTypeVariablesを指定することで,パターンに対して型シグネチャを書けるようになります(参考リンク1参考リンク2)。Hugsでは,-98オプションを使用した非互換モードで処理系を起動することで,同様の機能を利用できます(参考リンク)。

{-# LANGUAGE ScopedTypeVariables #-}
module Exception where
import Control.Exception
import Prelude hiding (catch)

catchError'' = catch (undefined) (\(e::SomeException) -> print "Caught error.")

Prelude> :r
[1 of 1] Compiling Exception        ( Exception.hs, interpreted )
Ok, modules loaded: Exception.
*Exception> catchError''
"Caught error."

 次に,型シグネチャとして使われているSomeExceptionについて見てみましょう。これは例外そのものを表現する型です。

Prelude Control.Exception> :i SomeException
data SomeException where
  SomeException :: forall e. (Exception e) => e -> SomeException
        -- Defined in GHC.Exception
instance Show SomeException -- Defined in GHC.Exception
instance Exception SomeException -- Defined in GHC.Exception

 SomeExceptionの型変数eが第20回で説明した存在型になっているのは,ある特定の例外ではなく,「Exceptionクラスのインスタンスとして定義されているあらゆる例外」を処理の対象とするためです。例えば,ゼロ除算による例外の代わりに元の値を返すコードは,SomeExceptionを使って以下のように書けます。

{-# LANGUAGE ScopedTypeVariables #-}
module Exception where
import Control.Exception
import Prelude hiding (catch)

catchException val = catch (evaluate $ val `div` 0) (\(e::SomeException) -> return val)

*Exception> catchException 100
100

 ただし,undefindやerrorとは異なり,第12回で紹介したControl.Exceptionモジュールのevaluate関数や第11回で紹介したparallelパッケージのControl.Parallel.StrategiesモジュールにあるNFDataクラスのrnfメソッドなどを使って,例外処理の対象になる式をあらかじめ評価しておかなければならない点に注意してください。

Prelude Control.Exception> :t evaluate
evaluate :: a -> IO a
Prelude Control.Exception> :m Control.Parallel.Strategies
Prelude Control.Parallel.Strategies> :i rnf
class NFData a where rnf :: Strategy a
        -- Defined in Control.Parallel.Strategies

 Haskellは遅延評価を行うので,例外の発生は値の評価が必要になるまさにそのときまで持ち越されます。

Prelude> fst ("good value", undefined)
"good value"
Prelude> snd ("good value", undefined)
*** Exception: Prelude.undefined
Prelude> head [0,undefined]
0
Prelude> [0,undefined]
[0,*** Exception: Prelude.undefined

 このせいで,例外が例外処理をすり抜けてしまうのです。例外処理をきちんと行うには,例外処理よりも前に例外が発生するよう,値の評価を強制する必要があります。

{-# LANGUAGE ScopedTypeVariables #-}
module Exception where
import Control.Exception
import Control.Parallel.Strategies
import Prelude hiding (catch)

catchException' val
  = catch (evaluate ([val `div` 0] `using` rnf)) (\(e::SomeException) -> return [val])

badCatchException   val = catch (return $ val `div` 0) (\(e::SomeException) -> return val)
badCatchException'  val = catch (return $ [val `div` 0]) (\(e::SomeException) -> return [val])
badCatchException'' val = catch (evaluate $ [val `div` 0]) (\(e::SomeException) -> return [val])

*Exception> badCatchException 100
*** Exception: divide by zero
*Exception> badCatchException' 100
[*** Exception: divide by zero
*Exception> badCatchException'' 100
[*** Exception: divide by zero
*Exception> catchException' 100
[100]