様々なI/Oエラー
I/Oエラーについてもう少し詳しく見ていきましょう。先に見た通り,GHCやHugsにおけるIOError型はIOException型と全く同じものとして定義されています。
Prelude> :i IOError type IOError = GHC.IOBase.IOException -- Defined in GHC.IOBase
2008年12月現在のIOException型は以下のように定義されています。この定義は,GHCの次のメジャーバージョンアップで変更される予定です(参考リンク)。
data IOException = IOError { ioe_handle :: Maybe Handle, -- the handle used by the action flagging -- the error. ioe_type :: IOErrorType, -- what it was. ioe_location :: String, -- location. ioe_description :: String, -- error type specific information. ioe_filename :: Maybe FilePath -- filename the error is related to. } deriving Typeable
IOException型の定義からわかるように,I/Oエラーは
- I/O処理に使用したハンドル(Maybe Handle)
- I/Oエラーの種類(IOErrorType)
- I/Oエラーの発生した場所(StringとMaybe FilePath)
- 利用者に可読性のあるエラー情報を伝えるための文字列
といった情報から構成されています。ただし,I/Oエラーは必ずしもファイル操作を行うI/O処理の中で発生するとは限りません。そこで,ハンドルを示す型はMaybe Handle,パス名を示す型はMaybe FilePath,といったようにMaybeに包まれた形になっています。
I/Oエラーにはどんな種類があるのでしょうか? IOErrorTypeなどで表現されるI/Oエラーには,Haskell 98で定義されているもののほかにGHCやHugsの独自拡張として用意されている以下のようなものがあります(参考リンク1,参考リンク2,参考リンク3)。
データ構成子 | 仕様 | エラー内容 |
---|---|---|
AlreadyExists | Haskell 98(alreadyExistsErrorType) | 作成する対象がすでに存在する |
ResourceBusy | Haskell 98(alreadyInUseErrorType) | 処理の対象とするリソースをすでに使用中 |
ResourceExhausted | Haskell 98(fullErrorType) | デバイスが満杯 |
NoSuchThing | Haskell 98(doesNotExistErrorType) | 処理の対象が存在しない |
EOF | Haskell 98(eofErrorType) | ファイルの終端に到達した |
IllegalOperation | Haskell 98(illegalOperationErrorType) | その操作は不可能 |
PermissionDenied | Haskell 98(permissionErrorType) | 処理を行う権限がない |
UserError | Haskell 98(userErrorType) | ユーザー定義のエラー |
UnsatisfiedConstraints | GHC拡張 | 処理を行うのに必要な制約が満たされていない |
SystemError | GHC拡張 | システム・エラー |
ProtocolError | GHC/Hugs拡張 | プロトコル・エラー |
OtherError | GHC/Hugs拡張 | その他のエラー |
InvalidArgument | GHC拡張 | 処理の対象の状態が正しくない |
InappropriateType | GHC拡張 | 処理の対象の種類が違う |
HardwareFault | GHC拡張 | (ハードウエアが原因の)物理的なエラー |
UnsupportedOperation | GHC/Hugs拡張 | (OSのAPIなどが)その処理に対応していない |
TimeExpired | GHC拡張 | 時間切れ |
ResourceVanished | GHC拡張 | 処理の対象が消えた |
Interrupted | GHC拡張 | 処理の中断 |
DotNetException | Hugs拡張 | .NET環境での例外 |
userError関数で作成するI/Oエラーは,上の表の中のUserErrorに相当します。
userError :: String -> IOError userError str = IOError Nothing UserError "" str Nothing
この表を見ていると,GHCとHugsで定義されているI/Oエラーの種類が異なっていることに気づくと思います。GHCでのみ提供されているものもあれば,Hugsでのみ提供されているものもあります。また,GHCやHugs以外の処理系では,IOError型やIOException型の定義も異なります。
I/Oエラーはどのように扱えばよいでしょうか? System.IO.Errorモジュールでは,IOErrorTypeやIOErrorを「定義を公開しない抽象データ型」の扱いにしており,代わりに,I/Oエラーの情報を取得したりI/Oエラーを作成したりするための様々な関数を提供しています(参考リンク1,参考リンク2)。
Prelude System.IO.Error> :browse alreadyExistsErrorType :: IOErrorType alreadyInUseErrorType :: IOErrorType annotateIOError :: IOError -> String -> Maybe GHC.IOBase.Handle -> Maybe FilePath -> IOError doesNotExistErrorType :: IOErrorType eofErrorType :: IOErrorType fullErrorType :: IOErrorType illegalOperationErrorType :: IOErrorType ioeGetErrorString :: IOError -> String ioeGetErrorType :: IOError -> IOErrorType ioeGetFileName :: IOError -> Maybe FilePath ioeGetHandle :: IOError -> Maybe GHC.IOBase.Handle ioeGetLocation :: IOError -> String ioeSetErrorString :: IOError -> String -> IOError ioeSetErrorType :: IOError -> IOErrorType -> IOError ioeSetFileName :: IOError -> FilePath -> IOError ioeSetHandle :: IOError -> GHC.IOBase.Handle -> IOError ioeSetLocation :: IOError -> String -> IOError isAlreadyExistsError :: IOError -> Bool isAlreadyExistsErrorType :: IOErrorType -> Bool isAlreadyInUseError :: IOError -> Bool isAlreadyInUseErrorType :: IOErrorType -> Bool isDoesNotExistError :: IOError -> Bool isDoesNotExistErrorType :: IOErrorType -> Bool isEOFError :: IOError -> Bool isEOFErrorType :: IOErrorType -> Bool isFullError :: IOError -> Bool isFullErrorType :: IOErrorType -> Bool isIllegalOperation :: IOError -> Bool isIllegalOperationErrorType :: IOErrorType -> Bool isPermissionError :: IOError -> Bool isPermissionErrorType :: IOErrorType -> Bool isUserError :: IOError -> Bool isUserErrorType :: IOErrorType -> Bool mkIOError :: IOErrorType -> String -> Maybe GHC.IOBase.Handle -> Maybe FilePath -> IOError modifyIOError :: (IOError -> IOError) -> IO a -> IO a permissionErrorType :: IOErrorType try :: IO a -> IO (Either IOError a) userErrorType :: IOErrorType ~ 略 ~
これらを使えば,処理系の定義に依存することなくI/Oエラーを扱えます。ただし,扱えるエラーの種類は,上の表で示したHaskell 98で定義されているI/Oエラーに限られます。
Prelude System.IO.Error> :browse alreadyExistsErrorType :: IOErrorType alreadyInUseErrorType :: IOErrorType ~ 略 ~ doesNotExistErrorType :: IOErrorType eofErrorType :: IOErrorType fullErrorType :: IOErrorType illegalOperationErrorType :: IOErrorType ~ 略 ~ permissionErrorType :: IOErrorType ~ 略 ~ userErrorType :: IOErrorType ~ 略 ~ userError :: String -> IOError
他の種類の例外を扱う場合には,処理系の種類やバージョンに依存することを承知したうえで,内部的な定義を使ってください。
では,個々の関数を説明しましょう。I/OエラーはmkIOErrorを使うことで作成できます。GHCやHugsではmkIOErrorは以下のように定義されています。
mkIOError :: IOErrorType -> String -> Maybe Handle -> Maybe FilePath -> IOError mkIOError t location maybe_hdl maybe_filename = IOError{ ioe_type = t, ioe_location = location, ioe_description = "", ioe_handle = maybe_hdl, ioe_filename = maybe_filename }
つまり,mkIOErrorにI/Oエラーの種類とエラーの発生個所を示す文字列,ハンドル,パス名を渡すことで,I/Oエラーを作成できます。利用者向けのメッセージは,基本的にはIOErrorTypeのShowクラスのインスタンスを使って作成します。
instance Show IOErrorType where showsPrec _ e = showString $ case e of AlreadyExists -> "already exists" NoSuchThing -> "does not exist" ResourceBusy -> "resource busy" ResourceExhausted -> "resource exhausted" EOF -> "end of file" IllegalOperation -> "illegal operation" PermissionDenied -> "permission denied" UserError -> "user error" HardwareFault -> "hardware fault" InappropriateType -> "inappropriate type" Interrupted -> "interrupted" InvalidArgument -> "invalid argument" OtherError -> "failed" ProtocolError -> "protocol error" ResourceVanished -> "resource vanished" SystemError -> "system error" TimeExpired -> "timeout" UnsatisfiedConstraints -> "unsatisified constraints" -- ultra-precise! UnsupportedOperation -> "unsupported operation"
Prelude System.IO.Error> mkIOError alreadyInUseErrorType "" Nothing Nothing resource busy *IOError System.IO.Error> mkIOError alreadyExistsErrorType "" Nothing Nothing already exists *IOError System.IO.Error> mkIOError alreadyExistsErrorType "ghci prompt" Nothing Nothing ghci prompt: already exists *IOError System.IO.Error> mkIOError alreadyExistsErrorType "ghci prompt" Nothing (Just "test/Exception") test/Exception: ghci prompt: already exists *IOError System.IO.Error> catchIOError $ ioError it "error caught." *IOError System.IO.Error> catchIOError' $ ioError $ mkIOError eofErrorType "" Nothing Nothing "error caught."
すでに作成したI/Oエラーに情報を追加したい場合にはannotateIOErrorを使います。
Prelude System.IO.Error> mkIOError alreadyExistsErrorType "" Nothing Nothing already exists Prelude System.IO.Error> annotateIOError it "ghci prompt" Nothing Nothing ghci prompt: already exists Prelude System.IO.Error> annotateIOError it "ghci prompt" Nothing (Just "test/Exception") test/Exception: ghci prompt: already exists Prelude System.IO.Error> annotateIOError it "this is example" Nothing Nothing test/Exception: this is example: already exists
この挙動から想像できるかもしれませんが,GHCやHugsでのannotateIOErrorの定義は以下のようになっています。
annotateIOError :: IOError -> String -> Maybe Handle -> Maybe FilePath -> IOError annotateIOError (IOError ohdl errTy _ str opath) loc hdl path = IOError (hdl `mplus` ohdl) errTy loc str (path `mplus` opath) where Nothing `mplus` ys = ys xs `mplus` _ = xs
I/Oエラーの情報を1個所だけ書き換えたい場合には,ioeSet*関数を使います。逆にI/Oエラーの情報を得るには,ioeGet*関数を使います。
Prelude System.IO.Error> mkIOError alreadyExistsErrorType "" Nothing Nothing already exists Prelude System.IO.Error> ioeSetLocation it "ghci prompt" ghci prompt: already exists Prelude System.IO.Error> ioeSetErrorType it permissionErrorType ghci prompt: permission denied Prelude System.IO.Error> ioeGetLocation it "ghci prompt"
ただしGHCやHugsでは,ioeSetErrorStringを使って書き込んだエラー情報をioeGetErrorStringを使って取得することはできないので注意してください。
Prelude System.IO.Error> mkIOError alreadyExistsErrorType "" Nothing Nothing already exists Prelude System.IO.Error> ioeSetErrorString it "this is example." already exists (this is example.) Prelude System.IO.Error> ioeGetErrorString it "already exists"
これは,2008年12月現在のGHCやHugsでのioeGetErrorStringの定義が,以下のようにIOErrorTypeを直接使用するものになっているためです。
ioeGetErrorString ioe | isUserErrorType (ioe_type ioe) = ioe_description ioe | otherwise = show (ioe_type ioe)
エラーの種類を判別するために,is*Errorやis*ErrorTypeという関数も用意されています。
*IOError System.IO.Error> let e = mkIOError illegalOperationErrorType "" Nothing Nothing *IOError System.IO.Error> isAlreadyExistsError e False *IOError System.IO.Error> isIllegalOperation e True *IOError System.IO.Error> isAlreadyExistsErrorType $ ioeGetErrorType e False *IOError System.IO.Error> isIllegalOperationErrorType $ ioeGetErrorType e True *IOError System.IO.Error> isDoesNotExistErrorType doesNotExistErrorType True *IOError System.IO.Error> isDoesNotExistErrorType fullErrorType False
これらを使うことで,I/Oエラーの種類に応じて異なる例外処理を行わせることができます。
module IOError where import System.IO.Error causeIOError'' = mkIOError fullErrorType "ghci prompt" Nothing Nothing catchIOError'' val = catch val (\e -> if isFullError e then print "error caught." else ioError e) catchIOError''' val = catch val (\e -> if isUserErrorType $ ioeGetErrorType e then print "error caught." else ioError e)
*IOError System.IO.Error> catchIOError'' $ ioError $ userError "error occur." *** Exception: user error (error occur.) *IOError System.IO.Error> catchIOError'' $ ioError causeIOError'' "error caught." *IOError System.IO.Error> catchIOError''' $ ioError $ userError "error occur." "error caught." *IOError System.IO.Error> catchIOError''' $ ioError causeIOError'' *** Exception: ghci prompt: resource exhausted