今回はテキストファイルの入出力処理について説明します。
Haskell では、ハンドル (Handle) を介してファイルにアクセスします。ハンドルはファイルと 1 対 1 に対応していて、ファイルからデータを入力するときは、ハンドルを経由してデータが渡されます。逆に、ファイルへデータを出力するときもハンドルを経由します。
通常のファイルはハンドルを生成しないとアクセスすることができません。ただし、標準入出力は Haskell の起動時にハンドルが自動的に生成されるので、簡単に利用することができます。
一般に、キーボードからの入力を「標準入力」、画面への出力を「標準出力」といいます。標準入出力に対応するハンドルはモジュール System.IO に定義されています。下表に変数名を示します。
変数名 | 型 | ファイル |
---|---|---|
stdin | Handle | 標準入力 |
stdout | Handle | 標準出力 |
stderr | Handle | 標準エラー出力 |
Handle はハンドルを表すデータ型です。「簡単な入出力」で説明した関数 getLine, readLn, putStr, putStrLn, print は stdin, stdout 専用の関数ですが、ハンドルを指定して入出力を行う関数も System.IO には用意されています。主な関数を下表に示します。
関数名 | 型 | 機能 |
---|---|---|
hGetChar | Handle -> IO Char | ハンドルから 1 文字読み込む |
hGetLine | Handle -> IO String | ハンドルから 1 行読み込む |
hGetContents | Handle -> IO String | ハンドルに含まれる内容を文字列にして返す |
hPutChar | Handle -> Char -> IO () | ハンドルに 1 文字書き込む |
hPutStr | Handle -> String -> IO () | ハンドルに 1 行書き込む |
hPutStrLn | Handle -> String -> IO () | ハンドルに 1 行書き込む (改行付き) |
hPrint | Handle -> a -> IO () | ハンドルにデータ型 a を表す文字列を書き込む |
hGetContents は遅延評価により、必要になったときにファイルからデータを読み込みます。つまり、返り値の文字列 (リスト) は遅延ストリームとして利用することができます。
簡単な実行例を示します。
ghci> :m + System.IO ghci> hGetChar stdin h'h' ghci> hGetLine stdin hello, world "hello, world" ghci> hPutChar stdout 'h' hghci> ghci> hPutStr stdout "hello, world" hello, worldghci> ghci> hPutStrLn stdout "hello, world" hello, world ghci> hPrint stdout 10 10 ghci> hPrint stdout "hello, world" "hello, world"
ファイルにアクセスする場合、次の 3 つの操作が基本になります。
「ファイルをオープンする」とは、アクセスするファイルを指定して、それと 1 対 1に対応するハンドルを生成することです。入出力関数はオープンしたハンドルを経由してファイルにアクセスします。
Haskell の場合、ファイルをオープンするにはモジュール System.IO に用意されている関数 openFile を使います。オープンしたファイルは必ずクローズしてください。この操作を行う関数が hClose です。openFile と hClose の型を示します。
openFile :: FilePath -> IOMode -> IO Handle hClose :: Handle -> IO ()
data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode type FilePath = String
openFile は引数にファイル名 (FilePath) とアクセスモード (IOMode) を指定して、ファイル名で指定されたファイルに対応する Handle を生成し、それを IO に格納して返します。アクセスモードは IOMode 型で指定します。下表にアクセスモードを示します。
モード | 動作 |
---|---|
ReadMode | 読み込み (read) モード |
WriteMode | 書き出し (write) モード |
AppendMode | 追加 (append) モード |
ReadWriteMode | 更新モード (読み書き両方が可能) |
読み込みモードの場合、ファイルが存在しないとエラーになります。書き出しモードの場合、ファイルが存在すれば、そのファイルを大きさ 0 に切り詰めてからオープンします。追加モードの場合、ファイルの最後尾にデータを追加します。
ファイル名は文字列で指定し、ファイル名のパス区切り記号にはスラッシュ ( / ) を使います。\ は文字列のエスケープコードに割り当てられているため、そのままではパス区切り記号に使うことはできません。ご注意ください。
それでは簡単な例題として、ファイルの内容を画面へ出力する関数 cat を作ってみましょう。プログラムは次のようになります。
リスト : ファイルの表示 (1) import System.IO cat :: FilePath -> IO () cat filename = do handle <- openFile filename ReadMode contents <- hGetContents handle putStr contents hClose handle
関数 cat の引数 filename はファイル名を表す文字列です。openFile で filename を ReadMode でオープンしてハンドルを変数 handle にセットします。次に、hGetContents でファイルの内容を読みこみ、それを putStr で画面に表示します。最後に hClose でハンドルをクローズします。
簡単な実行例を示します。test00.txt の内容を表示します。
hello, world hello, Haskell foo bar baz oops! oops! oops! abcd efgh ijkl 図 : test00.txt
ghci> cat "test00.txt" hello, world hello, Haskell foo bar baz oops! oops! oops! abcd efgh ijkl
もう一つ簡単な例題として、ファイルの先頭から n 行表示するように cat を変更してみましょう。関数名は cat' としました。次のリストを見てください。
リスト : ファイルの表示 (2) cat' :: Int -> FilePath -> IO () cat' n filename = do handle <- openFile filename ReadMode contents <- hGetContents handle mapM_ putStrLn $ take n $ lines contents hClose handle
lines は文字列を改行文字で分割する関数です。
lines :: String -> [String]
簡単な使用例を示します。
ghci> lines "foo bar baz" ["foo bar baz"] ghci> lines "foo\nbar\nbaz" ["foo","bar","baz"]
cat' は hGetContents で読み込んだ文字列を lines で分割し、take で n 行取り出して mapM_ putStrLn で表示するだけです。
簡単な実行例を示します。
ghci> cat' 1 "test00.txt" hello, world ghci> cat' 2 "test00.txt" hello, world hello, Haskell ghci> cat' 3 "test00.txt" hello, world hello, Haskell foo bar baz ghci> cat' 10 "test00.txt" hello, world hello, Haskell foo bar baz oops! oops! oops! abcd efgh ijkl
hGetContents を使わない場合は次のようになります。
リスト : ファイルの表示 (3) takeLines :: Int -> Handle -> IO [String] takeLines 0 _ = return [] takeLines n h = do eof <- hIsEOF h if eof then return [] else do x <- hGetLine h xs <- takeLines (n - 1) h return (x:xs) cat'' :: Int -> FilePath -> IO () cat'' n filename = do handle <- openFile filename ReadMode contents <- takeLines n handle mapM_ putStrLn contents hClose handle
関数 takeLines はハンドル h から n 行読み込み、それをリストに格納して返します。返り値の型は [String] ではなく IO [String] になることに注意してください。
ファイルからデータを読み込む場合、ファイルに格納されているデータには限りがあるので、ハンドルからデータを取り出していくと、いつかはデータがなくなります。この状態を「ファイルの終了 (end of file : EOF)」といいます。
最初に関数 hIsEOF を呼び出してファイルの終了 (EOF) をチェックします。hIsEOF の型を示します。
hIsEOF :: Handle -> IO Bool
EOF の場合は True を、そうでなければ False を返します。ただし、真偽値は IO に格納されて返されることに注意してください。
EOF の場合は return で空リストを IO に格納して返します。そうでなければ、hGetLine で 1 行読み込んで変数 x にセットします。次に takeLines で残りの (n - 1) 行を読み込んで変数 xs にセットします。最後に return で (x:xs) を IO に格納して返します。引数が 0 になったら return で空リストを IO に格納して返します。
cat'' は簡単です。ファイルを openFile でオープンし、そこから n 行を takeLines で読み込みます。あとは mapM_ putStrLn で読み込んだ行を標準出力に表示して、hClose でハンドルをクローズします。
データをファイルに書き込むには、ファイルを WriteMode でオープンします。このとき、注意事項が一つあります。既に同じ名前のファイルが存在している場合は、そのファイルの長さを 0 に切り詰めてからデータを書き込みます。既存のファイルは内容が破壊されることに注意してください。
それでは簡単な例題として、[String] の要素を 1 行ずつファイルに書き込む関数 outputStrings を作ってみましょう。次のリストを見てください。
リスト : ファイルの書き込み outputStrings :: FilePath -> [String] -> IO () outputStrings filename xs = do handle <- openFile filename WriteMode mapM_ (hPutStrLn handle) xs hClose handle
最初に openFile でファイル filename を WriteMode でオープンします。あとは、mapM_ を使ってリスト xs から要素を一つずつ取り出し、それを hPutStrLn でファイルに書き込みます。
簡単な実行例を示します。
ghci> outputStrings "test01.txt" ["hello, world", "foo", "bar", "baz", "oops"] ghci> cat "test01.txt" hello, world foo bar baz oops
ファイルをコピーするプログラムも簡単に作ることができます。次のリストを見てください。
リスト : ファイルのコピー copyFile :: FilePath -> FilePath -> IO () copyFile fname1 fname2 = do hin <- openFile fname1 ReadMode hout <- openFile fname2 WriteMode contents <- hGetContents hin hPutStr hout contents hClose hin hClose hout
引数 fname1 が入力ファイル名、fname2 が出力ファイル名を表します。最初に、fnam1 を ReadMode で、fname2 を WriteMode でモードでオープンします。次に、入力ファイルから hGetContents で内容を読み込み、hPutStr で出力ファイルへ書き出します。最後に hClose でファイルをクローズします。
簡単な実行例を示します。
ghci> copyFile "test00.txt" "test02.txt" ghci> cat "test02.txt" hello, world hello, Haskell foo bar baz oops! oops! oops! abcd efgh ijkl
ファイルのコピーは関数 readFile と writeFile を使うともっと簡単になります。関数の型を示します。
readFile :: FilePath -> IO String writeFile :: FilePath -> String -> IO ()
ファイルのオープンとクローズの処理を readFile と writeFile が行ってくれるので、プログラムはとても簡単になります。次のリストを見てください。
リスト : ファイルのコピー (2) copyFile' :: FilePath -> FilePath -> IO () copyFile' fname1 fname2 = readFile fname1 >>= writeFile fname2
readFile で読み込んだデータを演算子 >>= で取り出して writeFile に渡すだけです。
Haskell には、ファイルのオープンとクローズを自動的に行ってくれる便利な関数 withFile が用意されています。withFile の型を示します。
withFile :: FilePath -> IOMode -> (Handle -> IO r) -> IO r
withFile は第 1 引数にファイル名、第 2 引数にアクセスモード、第 3 引数に関数を指定します。withFile はファイルを指定されたアクセスモードでオープンし、そのハンドルを関数の引数に渡して呼び出します。withFile の実行が終了すると、ファイルのハンドルは自動的にクローズされます。何かしらのエラーが発生した場合でも withFile はファイルをクローズしてくれるので安心です。
簡単な例として cat'' と outputStrings を withFile を使って書き直してみましょう。プログラムは次のようになります。
リスト : withFile の使用例 cat''' :: Int -> FilePath -> IO () cat''' n filename = withFile filename ReadMode $ \handle -> takeLines n handle >>= mapM_ putStrLn outputStrings' :: FilePath -> [String] -> IO () outputStrings' filename xs = withFile filename WriteMode $ \handle -> mapM_ (hPutStrLn handle) xs
withFile に渡す関数はラムダ式を使うと簡単です。ラムダ式の引数 handle に withFile がオープンしたファイルのハンドルが渡されます。どちらの場合もファイルをクローズする処理を書く必要がないので、プログラムはとても簡単になります。
このほかにも、Haskell にはいろいろな入出力関数が用意されています。詳しい説明は Haskell のリファレンスマニュアルを参照してください。
Haskell の場合、モジュール System.Environment の変数 getArgs にコマンドラインで与えられた引数が格納されています。getArgs の型を示します。
getArgs :: IO [String]
簡単な実行例を示しましょう。次のリストを見てください。
リスト : コマンド引数の表示 (cmdline.hs) import System.Environment main :: IO () main = getArgs >>= print
cmdline.hs は変数 getArgs の内容を表示するだけです。3 つの引数を与えて起動すると、次のように表示されます。
$ stack ghc -- cmdline.hs [1 of 1] Compiling Main ( cmdline.hs, cmdline.o ) Linking cmdline.exe ... $ ./cmdline foo bar baz ["foo","bar","baz"]
簡単な例として、コマンドラインからファイル名を指定してファイルの内容を表示するプログラム cat.hs を作ってみましょう。次のリストを見てください。
リスト : ファイルの表示 (cat.hs) import System.Environment import System.IO main :: IO () main = do args <- getArgs case args of [] -> getContents >>= putStr _ -> mapM_ (\x -> readFile x >>= putStr) args
最初に getArgs でコマンドラインからファイル名を取得します。空リストの場合は標準入力 (stdin) からデータを getContents で読み込みます。そうでなければ、mapM_ で args からファイル名を取り出し、ラムダ式の中でファイルからデータを読み取って表示します。とても簡単ですね。
それでは cat.hs をコンパイルして、実際に実行してみましょう。
$ cat < test00.txt hello, world hello, Haskell foo bar baz oops! oops! oops! abcd efgh ijkl $ cat test00.txt hello, world hello, Haskell foo bar baz oops! oops! oops! abcd efgh ijkl $ cat test00.txt test01.txt hello, world hello, Haskell foo bar baz oops! oops! oops! abcd efgh ijkl hello, world foo bar baz oops $
正常に動作していますね。