PR

様々な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)。

データ構成子仕様エラー内容
AlreadyExistsHaskell 98(alreadyExistsErrorType)作成する対象がすでに存在する
ResourceBusyHaskell 98(alreadyInUseErrorType)処理の対象とするリソースをすでに使用中
ResourceExhaustedHaskell 98(fullErrorType)デバイスが満杯
NoSuchThingHaskell 98(doesNotExistErrorType)処理の対象が存在しない
EOFHaskell 98(eofErrorType)ファイルの終端に到達した
IllegalOperationHaskell 98(illegalOperationErrorType)その操作は不可能
PermissionDeniedHaskell 98(permissionErrorType)処理を行う権限がない
UserErrorHaskell 98(userErrorType)ユーザー定義のエラー
UnsatisfiedConstraintsGHC拡張処理を行うのに必要な制約が満たされていない
SystemErrorGHC拡張システム・エラー
ProtocolErrorGHC/Hugs拡張プロトコル・エラー
OtherErrorGHC/Hugs拡張その他のエラー
InvalidArgumentGHC拡張処理の対象の状態が正しくない
InappropriateTypeGHC拡張処理の対象の種類が違う
HardwareFaultGHC拡張(ハードウエアが原因の)物理的なエラー
UnsupportedOperationGHC/Hugs拡張(OSのAPIなどが)その処理に対応していない
TimeExpiredGHC拡張時間切れ
ResourceVanishedGHC拡張処理の対象が消えた
InterruptedGHC拡張処理の中断
DotNetExceptionHugs拡張 .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