mainからの実行

 さて,最後に(unsafeInterleaveIOのようなものではない)I/Oアクションがどこから起動されるのかを見てみましょう。GHCiのような対話環境でI/Oアクションが実行される場合には,プロンプトから起動されていると考えて問題ありません。そうでない場合にはどうなっているのでしょうか?

 答えは簡単です。Mainモジュールの値mainから起動されることになっています(参考リンク)。ただし,Mainモジュールを作成するのにモジュール宣言が必要なわけではありません。モジュール宣言を省略した場合には"module Main(main) where"と書かれているものと仮定されるからです(参考リンク)。

 mainの型はIO tです。よく勘違いされるように,IO ()でなければならないわけではありません。tの結果は廃棄されるため,実質IO ()と同じようなものですが,型検査のことを考えると,この違いは重要です。

 上のreadFileの定義に以下のような式を加えてIOtest.hsという名前で保存し,試してみることにしましょう。

main = Main.readFile "IOtest.hs"

 Hugsならrunhugs,GHCならrunhaskell(またはrunghc)を使うことでプログラムを実行できます。readFile関数をMainモジュールで修飾するのを忘れないでください。忘れると,以下のようにSystem.IOのものなのかMainのものなのかわからないというエラー・メッセージが表示されます。

IOtest.hs:4:7:
    Ambiguous occurrence `readFile'
    It could refer to either `readFile', defined at IOtest.hs:7:0
                          or `readFile', imported from System.IO at IOtest.hs:1:0-15

 それでは実行します。

*Main> :t main
main :: IO String
*Main> :q
Leaving GHCi.

$ runhugs IOtest.hs

$ runhaskell IOtest.hs
"import System.IO\nimport System.IO.Unsafe\n\nmain = Main.readFile \"IOtest.hs\"
\n\nreadFile :: String -> IO String\nreadFile f = openFile f ReadMode >>= \\h ->
\n             unsafeInterleaveIO (lazyRead h)\n\nlazyRead :: Handle -> IO Strin
g\nlazyRead h\n  = hIsEOF h >>= \\b ->\n    if b then\n       hClose h >>\n
  return []\n    else\n       hGetChar h >>= \\a ->\n       unsafeInterleaveIO (
lazyRead h) >>= \\as ->\n       return (a:as)\n"

 あれっ,runhaskellでは,readFileの実行結果であるStringの値が出てきてしまいました。親切心によるものなのかバグなのか判断できませんが,少なくとも期待していた動作ではありませんね。ghcを使ってコンパイルし,できた実行可能ファイルを実行してみることにしましょう。何もオプションを指定せずにghcを実行すると,Windowsではmain.exe,UNIX環境ではa.outという名前で実行可能ファイルが作成されます(なお,--makeオプションを指定すると,自動的にMainモジュールのファイル名と同じ名前を持つ実行可能ファイル(この場合にはIOtest.exeまたはIOtest)が作成されます)。

$ ghc IOtest.hs

$ main

 今度は何も出力されません,期待通りの動作です。

 それでは出力されるよう定義を書き換えてみましょう。ソースコードをそのまま出力するのも芸がないので,前節で説明したsequenceとmapMを使った例を示してみることにします。

main = let a = [0..9]
           b = map (\x -> putStr (show x) >> return (x + 1)) a
       in
       sequence b >>= \x ->
       putStr "\n" >>
       mapM (\y -> putStr $ " " ++ show y) x

 まず0から9までの数字を元に,mapを使ってI/Oアクションのリストを生成します。それぞれのI/Oアクションはx+1を返すので,次の行に出力される数字はスペースの後,上の行の数字に1を足したものになるはずです。なお,mapMの結果は捨てられるので,この関数をmapM_に置き換えてもかまいません。

$ ghc IOtest.hs

$ main
0123456789
 1 2 3 4 5 6 7 8 9 10

 無事,期待した通りに実行されているのがわかりますね。

 では,逆順のリストを作成するreverse関数を使って,リストaではなくリストbをひっくり返してみましょう。

main = let a = [0..9]
           b = map (\x -> putStr (show x) >> return (x + 1)) a
       in
       sequence (reverse b) >>= \x ->
       putStr "\n" >>
       mapM (\y -> putStr $ " " ++ show y) x

$ ghc IOtest.hs

$ main
9876543210
 10 9 8 7 6 5 4 3 2 1

 これもきちんと期待通りに動作します。

今回のまとめ

 今回は,どのようにすれば遅延評価とI/Oアクションを伴う命令型スタイルのプログラミングを共存させることができるか,について見ていきました。このような仕組みによって,難しいことを考えずともI/Oを使用できる実用的な言語になっているのです。

 また,IOモナドが単にそれらを共存させるだけではなく,その合成性によって命令型スタイルと関数型スタイルの架け橋となっていることも示しました。…とはいっても,まずはそんなに難しいことは考えずに,いろいろと遊んでみればよいと思います。IO型の入ったリストをいろいろと作ってsequenceで遊んでいるうちに,どんなことができそうか,だんだん見えてくるでしょう。今回説明しなかったSTMモナドを含め,他にどのような実例が存在するかについては,今後の回で語っていきたいと思います。

:mainコマンド

 GHC6.6では,GHCiに:mainコマンドが加わりました。これを使うと,対話環境内で値mainを実行できます(参考リンク)。runhaskellと同様に,IO tのtの値は切り落とされずに返されることになります。

*Main> :main
9876543210
 10 9 8 7 6 5 4 3 2 1[(),(),(),(),(),(),(),(),(),()]

 といっても,mainと入力することでも同じことができるので,これだけではどういう利点があるのかわかりませんね。

*Main> main
9876543210
 10 9 8 7 6 5 4 3 2 1[(),(),(),(),(),(),(),(),(),()]

 :mainコマンドの利点は,runhaskellやrunhugs,ビルドされたバイナリを実行するのと同様に,引数の入力を与えられるところにあります。System.EnvironmentモジュールのgetArgsを使って確かめてみましょう。

*Main> let main = System.Environment.getArgs >>= print
*Main> main foo bar

<interactive>:1:5: Not in scope: `foo'

<interactive>:1:9: Not in scope: `bar'
*Main> main "foo" "bar"

<interactive>:1:0:
    Couldn't match expected type `[Char] -> [Char] -> t'
           against inferred type `IO ()'
    In the expression: main "foo" "bar"
    In the definition of `it': it = main "foo" "bar"
*Main> :main foo bar
["foo","bar"]

 mainを実行する場合とは違い,:mainコマンドでは入力を受け取れるのがわかると思います。

 なお,:mainコマンドの先頭の文字は:moduleコマンドと被っているため,:mという省略形で呼び出すことはできません。どうしても省略したければ,:maまで打ち込んでください。

*Main> :m foo bar baz
syntax:  :module [+/-] [*]M1 ... [*]Mn
*Main> :ma foo bar baz
["foo","bar","baz"]

著者紹介 shelarcy

 今回の原稿を書き上げた直後,ふと気になってHaskell.orgで列挙されているいろいろな処理系(参考リンク)でのIO型およびIOモナドの定義を調べてみました。「多くの処理系では GHC のような環境渡し(environment passing)を暗黙に行う形の定義になっているのに対して,Hugsでは継続渡し形式(CPS:Continuation Passing Style)を使って定義されている」「いくつかの処理系では,IO型の中で直接エラーを取り扱えるようEither型を使って定義をしている」などなど処理系ごとの個性が見えてきて面白かったです。今回,StateモナドからIOモナドの定義は想像できるという風にあっさり流しましたが,興味のある方はソースをのぞいてみるとよいかもしれません。

 なお,HugsのIOモナドの実装は,HaskellではなくCで書かれています。とはいっても,冒頭で示した参考リンク先の論文にも継続上でのIOモナドの実装例が示されていますし,HBCという処理系にも(対話的モデル上の)継続渡し形式を使った実装が存在するので,それらを参考に大体の感じをつかむのは難しくないと思います(HBCのソースコードのディレクトリ構成は整理されていなくてわかりづらいので,HBCのソースを見る場合にはgrepなどを使って検索したほうがよいと思います)。