Haskell の IO は、ファーストクラス(第一級の値)だと解説されることがあります。その何が嬉しいのか、僕には長い間分かりませんでしたが、少し分かって来たので書いてみます。
IO がファーストクラスである証拠として、IO を返す関数がリストに格納される例がよく使われます。
[putStr "Hello ", putStr "World\n"]
こんな例を見せられても、何が嬉しいのかさっぱり分かりません。以下のように、最初に文字列を連結すればいいじゃんと思うからです。
putStr $ concat ["Hello ", "World\n"]
ちょっと嬉しい例
IO が絡むので、システム・プログラミングを考えてみましょう。
"unittest" というプログラムを、ファイル "data1"、"data2"、.. を引数として実行したいとします。IO がファーストクラスであることを活かしてないプログラムは以下のようになるでしょう。(以下のプログラムは、比較しやすいように冗長に書いています。)
import System.Cmd import System.Directory import Data.List main = do files <- getDirectoryContents "." let fs = filter ("data" `isPrefixOf`) files doTest fs doTest [] = return () doTest (f:fs) = do rawSystem "unittest" [f] doTest fs
doTest が再帰で書かれており、rawSystem が単独で使われています。rawSystem は、コマンドと引数のリストを取って、そのコマンドを実行する関数です。
rawSystem :: String -> [String] -> IO GHC.IOBase.ExitCode
ここまではいいですね?
再帰がよく分かるようになると、再帰を使わずに、高階関数である map などを使うようになります。
ここでハタと気付きます。「返り値が IO でも、map できるんだ!」と。
doTest fs = sequence_ $ map (\f -> rawSystem "unittest" [f]) fs
$ の右側は、以下のようなリストになります。
[rawSystem "unittest" ["data1"], rawSystem "unittest" ["data2"], ..]
IO にリストの操作が使えるので、リストの利点を享受できるようになります。以下は、"unittest" の仕様が変わって、設定ファイルデータファイルを取るようになった場合に、設定ファイルとデータファイルのすべての組み合わせを試すプログラムです。
main = do files <- getDirectoryContents "." let cs = filter ("config" `isPrefixOf`) files fs = filter ("data" `isPrefixOf`) files doTest cs fs doTest cs fs = let as = [[c,f]|c<-cs,f<-fs] in sequence_ $ map (rawSystem "unittest") as
いやぁ、便利だ。