ある言語で書かれたライブラリは,その言語の仕様やその言語で使われている一般的な慣習に従った形でエラーまたは例外を通知します。そのため,例外を通知するための方法について特に悩む必要はありません。
しかし,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エラーを捕捉できていることもわかりますね。