HaskellのファイルIO

(2009.5.12) ページを分割し、加筆。

(2017.3) 全体的に書き直し。今どきの内容に更新。

Haskellでファイルを読み書きしたりする方法について。例外についても、少し解説。

テキストモード、バイナリモード

(2017.3 追加)

Haskell のファイルIOでは、ファイルを開くときに、テキストモードとバイナリモードがある。

テキストモード

テキストモードでは, Windows環境では, 読み込み時に CRLF -> LF変換、書き込み時に逆の変換が行われる。UNIX 環境では、テキストファイルの改行コードが CRLFであっても、特に何も変換されない。

テキストモードでは, ファイルの文字コードが設定される。Haskell の文字列は Unicodeだった。ファイルを読み込むときに, ファイルの文字コードに沿って Unicode テキストに変換され, String などに格納される。

ファイルを開いた時点では, 文字コードは, 実行時のロケールのそれと仮定される. ハンドルごとに, hSetEncoding 関数で変更できる。

テキストファイルを軽く扱うような場合はテキストモードを使う。

バイナリモード

一方、バイナリモードでは、改行コードの変換は行われず、ファイルはただの8bitのバイト列として取り扱われる。

テキストだけれども文字コードが不明など難しい状況を扱うときにも, バイナリモードを用いる。

バイナリモードのファイルを読み込むときは, ByteString を使うこと。型としては String で読み込むこともできてしまうが、1バイトずつ Char に格納されてしまう。

readFile, writeFile

簡単にテキストファイルを読み込むには、Prelude モジュールの readFile 関数を呼び出します。型はこう;

Haskell
[POPUP]
  1. readFile :: FilePath -> IO String
  2. type FilePath = String

readFileの第一引数はファイル名で、ファイルを開くところからしてくれます。実際の読み込みは遅延評価され、後工程で必要でない部分は読み込まれません。

ただ, String型は廃れているので, 新しいプログラムでは Text型のほうがいいでしょう。Data.Text.IOモジュールの同名関数を使います。

バイナリモードでバイト列として読み込むには, Data.ByteStringモジュールの同名関数を使います。

ファイルに書き込むには, writeFile 関数を呼び出します。宣言;

Haskell
[POPUP]
  1. writeFile :: FilePath -> String -> IO ()

次の例は単にファイルの内容を表示します。

Haskell
[POPUP]
  1. main :: IO ()
  2. main = do
  3. cs <- readFile "03readfile.hs"
  4. putStr cs

次の例はファイルの先頭5行のみを表示します。必要でない部分は読み込まれません。

Haskell
[POPUP]
  1. doHead :: Int -> String -> String
  2. doHead n cs =
  3. unlines $ take n $ lines cs
  4. main :: IO ()
  5. main = do
  6. cs <- readFile "maybe.hs"
  7. putStr $ doHead 5 cs

ファイルハンドル

readFile, writeFile は大雑把すぎます。

ファイルを開くときに読み書きモードにしたり, 1行ずつ自分で制御したいときなどは System.IO モジュールの関数を用います。C++での <cstdio> 相当です。

ちなみに、より細かく制御したいときは, unix パッケージの System.Posix.IO モジュールを使います。UNIX (POSIX) の open(2) 相当です。ここでは割愛します。

話を戻して, 名前からだいたい何をするものか見当がつきます。

Haskell
[POPUP]
  1. openFile :: FilePath -> IOMode -> IO Handle
  2. data IOMode = ReadMode
  3. | WriteMode
  4. | AppendMode
  5. | ReadWriteMode
  6. hClose :: Handle -> IO ()
  7. hGetChar :: Handle -> IO Char
  8. hGetLine :: Handle -> IO String
  9. hPutChar :: Handle -> Char -> IO ()
  10. hPutStr :: Handle -> String -> IO ()
  11. hPutStrLn :: Handle -> String -> IO ()

ファイルを開く

ファイルを開くのに, openFile 関数と openBinaryFile があります。前者がテキストモード, 後者がバイナリモードになります。

型は同じです。

このほか, 一時ファイルを扱うために, openTempFile 関数, openBinaryTempFile 関数があります。

ファイルの読み込み

テキストモードのときは, System.IO モジュールにある String を返す関数 (古い、非推奨) か、Text 型を返す Data.Text.IO モジュールの同名関数を使います。

バイナリモードのときは, Data.ByteStringモジュールの, ByteString を返す関数を使うこと。

1行読み込むのは hGetLine関数です。改行コードの '\n' は、読み込んだ文字列から削除され, 戻り値には含まれません。そのため, ファイル末尾の '\n' の有無を区別できません。

EOFに達していてさらに呼び出すと, 例外が発生します。

次の例は, テキストファイルから1行ずつ読み込み、単に表示します。

Haskell
[POPUP]
  1. import System.IO
  2. import Data.Text.IO as DTI
  3. -- EOFに到達し、さらに読もうとすると、例外が発生する
  4. get_and_print :: Handle -> IO ()
  5. get_and_print fp = do
  6. eof <- hIsEOF fp
  7. if eof then return ()
  8. else do
  9. str <- DTI.hGetLine fp
  10. print str
  11. get_and_print fp
  12. main :: IO ()
  13. main = do
  14. fp <- openFile "crlf.txt" ReadMode
  15. get_and_print fp

例外の捕捉

ファイルが見つからないときは実行時エラー (例外) が投げられます。例外はcatch関数で捕まえます。

Control.Exceptionモジュール (baseパッケージ) で, catch関数が宣言されている。catch関数の型は、

Haskell
[POPUP]
  1. catch :: Exception e => IO a -> (e -> IO a) -> IO a

catchの第一引数が例外が発生するかもしれない処理、第2引数がエラーハンドラ関数です。

例外が発生したときは, 第1引数のなかの処理が中断され, 第2引数の関数が呼び出されて、catchの戻り値はその関数の戻り値になります。

例外が発生した箇所には復帰しません。

次の例は、ファイルが見つからなかったらエラーメッセージを表示します。

(2017.3) 最近のGHCでのコンパイルエラーを修正。

Haskell
[POPUP]
  1. import System.Environment -- getArgs
  2. import System.IO -- hPutStrLn, stderr
  3. import Control.Exception -- catch (from 'base' package)
  4. onError :: String -> IOError -> IO String -- (1)
  5. onError filename error_ = do
  6. hPutStrLn stderr $ filename ++ " :: " ++ (show error_)
  7. return []
  8. main :: IO ()
  9. main = do
  10. args <- getArgs -- (2)
  11. content <- if null args -- null はリストが空なら真を返す
  12. then getContents
  13. else catch (readFile $ head args)
  14. (onError $ head args)
  15. putStr content

(1) catchの第2引数として渡すために, ここでは onErrorを定義しています。戻り値の型は、catchの第1引数に合わせます. readFileが型 IO Stringなので、同じにします。

(2) getArgs関数はコマンドライン引数の配列を返します。

コマンドの引数が与えられていないときは標準入力から読み込み、引数があるときはそのファイルを読み込みます。

この例は作為的で、普通ならputStrも例外が発生するかもしれない処理のなかに書くところです。

新しい例外機構

これまでの例外の難点を解消した safe-exceptions パッケージが出ています。

# cabal install --dry-run safe-exceptions
Resolving dependencies...
In order, the following would be installed (use -v for more details):
transformers-compat-0.5.1.4
exceptions-0.8.3
safe-exceptions-0.1.4.0
See https://haskell-lang.org/tutorial/exception-safety

TODO: 何か書く。