第9章はIOの続き。ファイルの読み書き、乱数生成、コマンドライン引数について。前の章と違って具体例が中心である。
ファイルとストリーム
ストリームはプログラムに出入りするデータの流れのことを言う。
runhaskell capslocker.hs < haiku.txt
のようにしてファイルを標準入力から取り込むことを考える。
ハスケルではファイルからの入力も遅延評価できる。
import Control.Monad import Data.Char main = forever $ do l <- getLine putStrLn $ map toUpper l -- (だいたい)等価 import Data.Char main = do contents <- getContents putStr $ map toUpper contents
getContentsは遅延評価されるので、インプットのテキストやターミナルからの入力を必要になった時に取り込むようになっている。
-- 複数行からなるインプットをとって10文字以内の行のみを出力する main = do contents <- getContents putStr $ shorLinesOnly contents shortLinesOnly :: String -> String shortLinesOnly = unlines . filter (\line -> length line < 10) . lines -- 等価 main = interact shortLinesOnly
interactはStringをとってStringを返す関数をとって、IOアクションを返す。このIOアクションは入力に引数にとった関数を適用した結果を表示する。
ファイルの読み書き
前のパートで標準入力、出力での読み書きが分かったのでファイルからの入出力を考える。ファイルからの入出力の概念は標準入出力とあまり変わらない。
import System.IO main = do handle <- openFile "girlfriend.txt" ReadMode contents <- hGetContents handle putStr contents hClose handle -- 等価 main = do withFile "girlfriend.txt" ReadMode (\handle -> do contents hGetContents handle putStr contents)
このプログラムで目新しいのはopenFile、h...である。openFileの型シグネチャはFilePath -> IOMode -> IO Handle
、FilePathはStringの型シノニム、IOModeはdata IOMode = ReadMode | WriteMode | AppendMode " ReadWriteMode
というデータ型になっている。hGetContentsとhCloseはどのファイルを取り扱うか示すものである、ハンドルを引数に取る。
withFileの型シグネチャはFilePath -> IOMode -> (Handle -> IO a) -> IO a
となっている。withFileを使えばハンドルを開放し忘れることはない。withFileはbracketを使って実装してみると
import Control.Exception withFile :: FilePath -> IOMode -> (Handle IO a) -> IO a withFile filename mode f = bracket (openFile filename mode) (\handle -> hClose handle) (\handle -> f handle)
のようになる。bracketはIO a -> (a -> IO b) -> (a -> IO c) -> IO c
で2つ目の引数はエラーが起こった時に呼ばれる関数、3つ目の引数は問題ない時に呼ばれる関数である。
テキストファイルの読み書きはよくある操作なので専用の関数もある。readFile、writeFile、appendFileである。つまり、
import System.IO -- readFileはPreludeに含まれているのでimport不要では? main = do contents <- readFile "girlfriend.txt" putStr contents
コマンドライン引数
System.EnvironmentのgetArgs関数を使う。型シグネチャはIO[String]
で、コマンドライン引数のリストを返す。(getProgName::IO Stringというものもあり、プログラムの名前を返す。)
具体例
import System.Environment import System.Directory import System.IO import Data.List -- 作業内容選択用のリスト dispatch :: [(String, [String] -> IO ())] dispatch = [ ("add", add) , ("view", view) , ("remove", remove) ] -- Maybeを使う -- lookup :: Eq a => a -> [(a, b)] -> Maybe b main = do (command:args) <- getArgs let (Just action) = lookup command dispatch action args add :: [String] -> IO () add [fileName, todoItem] = appendFile fileName (todoItem ++ "\n") -- ファイルから読み取った行の先頭に数字をzipしてから表示する view :: [String] -> IO () view [fileName] = do contents <- readFile fileName let todoTasks = lines contents numberedTasks = zipWith (\n line -> show n ++ " - " ++ line) [0..] todoTasks putStr $ unlines numberedTasks -- 一時作業用ファイルを作って作業する remove :: [String] -> IO () remove [fileName, numberString] = do handle <- openFile fileName ReadMode (tempName, tempHandle) <- openTempFile "." "temp" contents <- hGetContents handle let number = read numberString todoTasks = lines contents newTodoItems = delete (todoTasks !! number) todoTasks hPutStr tempHandle $ unlines newTodoItems hClose handle hClose tempHandle removeFile fileName renameFile tempName fileName
-- エラーハンドリングありdispatch dispatch :: String -> ( [String] -> IO () ) dispatch command | command == "add" = add | command == "view" = view | command == "remove" = remove | otherwise = doesntExist command doesntExist :: String -> String -> IO () doesntExist command _ = putStrLn "The " ++ command ++ " command doesn't exist." main = do (command:args) <- getArgs dispatch comannd args
-- エラーハンドリングありremove import Control.Exception remove :: String -> IO () remove [fileName, numberString] = do contents <- readFile filename let todoTasks = lines contents numberedTasks = zipWith (\n line -> show n ++ " - " ++ line) [0..] todoTasks putStrLn "These are your TODO items:" mapM_ putStrLn numberedTasks let number = read numberString newTodoItems = unlines $ delete (todoTasks !! number) todoTasks bracketOnError (openTempFile "." "temp") (\(tempName, tempHandle) -> do hClose tempHandle removeFile tempName) (\(tempName, tempHandle) -> do hPutStr tempHandle newTodoItems hClose tempHandle removeFile fileName renameFile tempName fileName)
オンライン版をコピペしてきたものだが手元で印刷した古いバージョン(ハードコーディングしてあったファイル名の書き換え忘れ間違いがあった...)と違うところは最後に書き足した。(オンライン版はMaybeを使っている。自分はこちらできちんと動くならこの書き方のほうが好きと感じる。)
removeについて補足すると、openTempFileという関数が使われている。openTempFile :: FilePath -> String -> IO (FilePath, Handle)
で、上の例では"."で現在のディレクトリ、"temp"でファイル名が指定され一時ファイルを作成するアクションと、返り値としてファイルの名前とハンドルを返す。Control.ExceptionのbracketOnErrorがエラーハンドリングバージョンとして作られている。bracketOnErrorはbracketと違い、エラーがなければハンドルを開放しない。
乱数生成
System.Randomをインポートして利用する。random :: (RandomGen g, Random a) => g -> (a, g)
である。random関数に乱数ジェネレータgを与える。mkStdGen関数はInt -> StdGen
である。
import System.Random -- 型指定が必須 > random (mkStdGen 100) :: (Int, StdGen) -- 乱数と乱数ジェネレータが返り値 (-3650871090684229393,693699796 2103410263) > random (mkStdGen 100) :: (Int, StdGen) -- 同じ値 (-3650871090684229393,693699796 2103410263)
無限長のランダム値を手に入れるにはrandomsrandoms :: (RandomGen g, Random a) => g -> [a]
を使う。randomsは再帰的に定義されているので乱数ジェネレータを返すことはない。
take 5 $ randoms (mkStdGen 11) :: [Int]
ある範囲の乱数を生成するにはrandomRrandomR :: (RandomGen g, Random a) => (a, a) -> g -> (a, g)
を使う。
randomR (1, 6) (mkStdGen 100) -- 無限長ver -- この場合だと型指定がなくても通る take 3 $ randomRs ('a', 'z') (mkStdGen 3) :: [Char]
IOで乱数ジェネレータを作る
上の方法では乱数の種が毎回同じだったので結果が変わらないことになる。それでは困るのでIOアクションを使って違う乱数ジェネレータを手に入れたい。
import System.Random main = do gen <- getStdGen putStrLn $ tak 3 (randomRs (0, 10) gen) -- 全く同じ結果 gen <- getStdGen putStrLn $ tak 3 (randomRs (0, 10) gen)
getStdGenを利用すると実行時ごとに違う値を使えるが実行中はジェネレータを更新できない。newStdGenを使って乱数ジェネレータを更新する。
main = do gen <- getStdGen putStrLn $ tak 3 (randomRs (0, 10) gen) -- 異なる結果 gen <- newStdGen putStrLn $ tak 3 (randomRs (0, 10) gen)
Bytestring
「評価法が指示されているが実際の計算が行われていない中間状態の時それをプロミス (promise) や、計算の実体をさしてサンク (thunk) といい、プロミスを強制(force)することで値が計算される。」 http://t.co/YqBrWarF9P
— 山本和彦 (@kazu_yamamoto) 2013, 4月 12
だそうです。そしてこのサンクでただの数のリストをサンクの列として処理するのは非効率。そこでbytestringを使う。普通のリストは型が[a]だがbytestringではByteString型を、aの代わりにWord8をやりとりする。Word8は8ビット符号なし整数なので0から255までの整数でNumクラスのインスタンスになっている。なので5などの数字は多層型なのでWord8としても振る舞えることからそのまま使うことが出来る。
-- bytestringには正格バージョンと遅延バージョンがあるのでimportするときに好きな方を選んで使う。 import qualified Data.ByteString.Lazy as B import qualified Data.ByteString as S
遅延bytestringは要素ごとにサンクになるのではなく、L2キャッシュに乗るように64Kバイト単位でチャンク(chunk)としてまとめてある。正格bytestringのリストと見ると分かりやすい。
ByteStringの生成と値の取り出し方。ghciでの表示がテキストと異なるのでshowあたりの実装が変わったのかもしれない。
-- pack :: [Word8] -> ByteString > B.pack [99, 97, 110] -- テキストにはChunk "can" Emptyとあるが手元の環境では"can"のみが帰ってくる。 "can" > B.unpack $ B.pack [97, 98, 110] [97, 98, 110]
正格、遅延Bytestringの変換も可能。どちらもData.ByteString.Lazyの関数を使う。bytestring同士の連結もB.consで出来るようになっている。
-- 正格 -> 遅延 > B.fromChunks [S.pack [40, 41, 42], S.pack [43, 44, 45], S.pack [46, 47, 48]] "()*+,-./0" -- 遅延 -> 正格 > B.toChunks $ B.pack [40, 41, 42] ["()*"] > B.cons 85 $ B.pack [80, 81, 82] -- 85 `B.cons` B.pack [80, 81, 82]の方が分かりやすい? "UPQR"
普通にリストを使ったプログラムのパフォーマンスに問題があればそれから書きなおせばいい。(実際に使う場合は文字コードがらみで注意点があるらしいので日本語訳を参照する。)