ある言語で書かれたライブラリは,その言語の仕様やその言語で使われている一般的な慣習に従った形でエラーまたは例外を通知します。そのため,例外を通知するための方法について特に悩む必要はありません。

 しかし,FFIを使って他の言語からライブラリを使おうとした瞬間,このような約束事は通用しなくなります。別の言語で書かれた関数は,別の言語の仕様や慣習に従った形でエラーまたは例外を通知するからです。FFIを使って呼び出す処理がエラーや例外を発生させる場合,エラーや例外を適切なものに変換することで整合性を取る必要があります。今回は,その方法と,そのために提供されている仕組みを説明します。

例外としてのI/Oエラー

 Haskellで扱う例外には,Haskell独自のものもあれば,他の言語環境(C言語でOSやライブラリのAPIを利用する場合など)でも発生する可能性があるものもあります。Haskell独自の例外に対しては,Haskellの内部で完結した例外処理を行わなければなりません。一方,Haskell以外の言語環境に共通する例外であれば,FFI越しに呼び出す言語環境にエラー処理や例外処理を任せてもかまわないでしょう。

 他の言語環境と共通した例外の一つに,主にOSのAPIの使用時に発生するI/Oエラーがあります。まず,I/OエラーのHaskellでの扱いについて見ていきましょう。I/Oエラーは,PreludeではIOError型,Control.ExceptionではIOException型として定義されています。

Prelude> :i IOError
type IOError = GHC.IOBase.IOException   -- Defined in GHC.IOBase
Prelude> :m Control.Exception
Prelude Control.Exception> :i IOException
data IOException
  = GHC.IOBase.IOError {GHC.IOBase.ioe_handle :: Maybe
                                                   GHC.IOBase.Handle,
                        GHC.IOBase.ioe_type :: GHC.IOBase.IOErrorType,
                        GHC.IOBase.ioe_location :: String,
                        GHC.IOBase.ioe_description :: String,
                        GHC.IOBase.ioe_filename :: Maybe FilePath}
        -- Defined in GHC.IOBase
instance Eq IOException -- Defined in GHC.IOBase
instance Show IOException -- Defined in GHC.IOBase
instance Exception IOException -- Defined in GHC.IOBase

 Haskell 98は一般的な例外処理は定義しておらず,IOモナド内でのエラーに対する処理しか定めていません。このため,Preludeでのcatch関数は,I/Oエラーに対してのみ定義されています(参考リンク)。

Prelude> :i catch
catch :: IO a -> (IOError -> IO a) -> IO a      -- Defined in Prelude

 実際にはControl.Exceptionモジュールのcatch関数の方がより広範な例外に対して処理を行うことができます。第12回および第25回で,PreludeではなくCotrol.Exceptionのcatch関数を使用するよう指示したのはこのためです。

 Preludeには,I/Oエラーを発生させるioError関数と,文字列からI/Oエラーを作成するuserError関数も用意されています。

Prelude> :i ioError
ioError :: IOError -> IO a      -- Defined in GHC.IOBase
Prelude> :i userError
userError :: String -> IOError  -- Defined in GHC.IOBase

 GHCやHugsでは,これらの関数は,より一般化されたControl.Exceptionの例外処理の仕組みを利用して実装されています。以下にGHCやHugsの例を示します。

#ifndef __HUGS__
import qualified Control.Exception.Base as New (catch)
#endif

#ifdef __HUGS__
import Hugs.Prelude
#endif

~ 略 ~

#ifndef __HUGS__
~ 略 ~
catch :: IO a -> (IOError -> IO a) -> IO a
catch = New.catch
#endif /* !__HUGS__ */

module Hugs.Prelude (

~ 略 ~

catch :: IO a -> (IOError -> IO a) -> IO a
catch m h = catchException m $ \e -> case e of
                IOException err -> h err
                _ -> throw e

 #ifndefや#ifdefを使って実装を分けていることからわかるように,GHCとHugsでは多少異なった方法でPreludeのcatch関数を定義しています。GHCではControl.Exception(.Base)モジュールで定義されているcatch関数の例外型をIOError型に束縛する形で呼び出しています。これに対し,HugsではControl.Exception(.Base)モジュールのcatch関数の内部実装に使われるcatchExceptionを利用することで,I/Oエラーに対するcatch関数を直接定義しています。両者の機能は全く同じになります。

ioException     :: IOException -> IO a
ioException err = throwIO err

-- | Raise an 'IOError' in the 'IO' monad.
ioError         :: IOError -> IO a 
ioError         =  ioException

~ 略 ~

userError       :: String  -> IOError
userError str   =  IOError Nothing UserError "" str Nothing

 ioError関数の実装に使われているthrowIOは,Control.Exceptionモジュールで提供されているthrowのI/Oアクション版です。

Prelude Control.Exception> :i throwIO
throwIO :: (Exception e) => e -> IO a   -- Defined in GHC.IOBase

 throwとthrowIOには以下のような違いがあります(参考リンク)。

throw e   `seq` x  ===> throw e
throwIO e `seq` x  ===> x

 throwはどこでも例外を発生させられるのに対し,throwIOはIOモナドの内部でしか例外を発生させることができません。この違いにより,throwIOはI/O処理内での実行順序を保証した形で例外を発生させます。同様に,ioErrorもI/O処理内での実行順序を保証した形でI/Oエラーを発生させます。

 実際にI/Oエラーを使ってみましょう。

{-# LANGUAGE ScopedTypeVariables #-}
module IOError where
import Control.Exception hiding (catch)
import qualified Control.Exception as CE

causeIOError = ioError $ userError "error occur."
causeIOError' = do
    print "Before exception: this must show."
    causeIOError
    print "After exception: this must not show."

catchIOError val = catch val (\_ -> print "error caught.")
catchIOError' val = CE.catch val (\(e::IOError) -> print "error caught.")

*IOError> causeIOError
*** Exception: user error (error occur.)
*IOError> causeIOError'
"Before exception: this must show."
*** Exception: user error (error occur.)
*IOError> catchIOError causeIOError
"error caught."
*IOError> catchIOError causeIOError'
"Before exception: this must show."
"error caught."
*IOError> catchIOError' causeIOError
"error caught."
*IOError> catchIOError' causeIOError'
"Before exception: this must show."
"error caught."

 二つのI/O処理の間で,IOモナドで記述した順番通りにI/Oエラーが発生していることがわかります。PreludeやControl.Exceptionのcatch関数を使ってI/Oエラーを捕捉できていることもわかりますね。