Hakell I/Oアクションの紹介
From HaskellWiki
Haskellでプログラミングするときに副作用があるような処理、あるいは外部に対して働きかけるような処理を書きたいときは アクション を使います。
アクションはHaskellの言語仕様の中では、3という数字やつまり変数名に束縛したり、関数に引数として与えたり、関数の結果とすることが可能ということです。 Haskellが扱う他の値と同様に、アクションにも型があります。多くの種類のアクションがありますが、ここでは IO アクションと呼ばれる非常に重要なアクションから始めましょう。 このアクションはプログラムの外部に対して働きかけることができるアクションです。ここにIOアクションの例を示します:
- コンソールに "hello" という文字列を表示する
- コンソールから入力行を読み取る
- www.google.comに対して80番ポートでネットワーク接続を確立する
- ターミナルから2行入力を読み込んで、数字として処理し、足し算をして結果を表示する
- マウスの動きを入力として、スクリーンにグラフィックを表示するファーストパーソン・シューティングゲーム
以上をみてわかるように、IOアクションは非常に単純なこと(文字列を出力する)から非常に複雑なこと(テレビゲーム)まで多岐にわたります。 またIOアクションはHaskellプログラムで使われる値として結果を残すことも可能であるということに気付いたと思います。 コンソールから入力行を読み取る処理でポイントとなるのは、プログラムにデータを渡す部分です。 アクション型は値型のように結果として提示するもの(たとえばString)と同様にアクションの種類(IO)も反映します。 たとえば、コンソールから入力行を読み取るというアクションは IO String という型を持っています。実際、すべてのIOアクションは a という結果の型に対して IO a という型を追っています。 アクションがプログラムにとくに結果を返さない場合は、結果を表すのにユニット型( () と表記されます)が使われます。 C, C++, Javaといったプログラミング言語を知っている人はユニット型が"void"型の返り値と似たものだと思って下さい。上で述べたIOアクションには次のような型があります:
- コンソールに "hello" という文字列を表示する: IO ()
- コンソールから入力行を読み取る: IO String
- www.google.comに対して80番ポートでネットワーク接続を確立する: IO Socket
- ターミナルから2行入力を読み込んで、数字として処理し、足し算をして結果を表示する: IO Int
- マウスの動きを入力として、スクリーンにグラフィックを表示するファーストパーソン・シューティングゲーム: IO ()
アクションがプログラムに使われる値を結果として返す一方で、引数には一切とりません。 putStrLn を考えてみましょう。 putStrLn は次のような型を持っています:
putStrLn :: String -> IO ()
x = putStrLn "hello"
module Main where main :: IO () main = putStrLn "hello"
1 注意してください
「じゃあ1つのIOアクションしか実効できないなら、どうやってHaskellプログラムで実用的な事をさせられるんだろう」と疑問に思うかもしれません。前のほうで述べたように、IOアクションというのは非常に複雑です。たくさんの簡単なアクションをつなげて、複雑なアクションを作ることができます。アクションをつなげるために doブロック を使います。
doブロックは2つ以上のアクションを1つのアクションとしてつなげるものです。たとえば2つのIOアクションが結合されてその返り値がIOアクションだった場合、実行されるとまず最初のアクションが処理されて、その後に2番目のアクションが実行されます。簡単な例を見てみましょう。
main :: IO () main = do putStrLn "hello" putStrLn "world"
main は"hello"を表示して、その後 "world" を表示するアクションです。 もし最初のアクションが副作用を持っていても、2つ目のアクション実行時にはその副作用を観測できる状態にあります。 たとえば、最初のアクションでファイルに書き込みが行われて、2番目のアクションで読み込みが行われたとき、ファイルの変更点は読み込みに反映されます。
IOアクションはプログラムに結果を返すことができると言ったのを思い出してください。doブロックの結果はdoブロックの最後のアクションの結果となります。先ほどの例では、最後のアクション(doブロックはあるアクションの結果を他のアクションの生成のために使うこともできます。たとえばこんな具合です:
main:: IO () main = do line <- getLine -- line :: String putStrLn ("you said: " ++ line)
これまでdoブロックを2つのアクションを繋げるために使ってきました。これによりもっと多くのアクションを繋ぐことも可能とわかります:
main :: IO () main = do putStrLn "Enter two lines" do line1 <- getLine -- line1 :: String do line2 <- getLine -- line2 :: String putStrLn ("you said: " ++ line1 ++ " and " ++ line2)
幸いなことに、こんな面倒な書き方をする必要はありません。doブロックでは複数のアクションを1つのブロックに書き下すことができます。 マルチアクション・ブロックの意味は前述の入れ子の例をみれば明らかだと思います。束縛はそれ以降の全てのアクションから観測可能です。先の例はこのようにコンパクトに書き直すことができます。
main :: IO () main = do putStrLn "Enter two lines" line1 <- getLine -- line1 :: String line2 <- getLine -- line2 :: String putStrLn ("you said: " ++ line1 ++ " and " ++ line2)
promptLine :: String -> IO String pormptLine prompt = do putStr prompt getLine main :: IO () main = do line1 <- promptLine "Enter a line: " -- line1 :: String line2 <- promptLine "And another: " -- line2 :: String putStrLn ("you said: " ++ line1 ++ " and " ++ line2)
2行読みこんで、それらを結合して返すような便利な関数を書いてみましょう。
promptTwoLines :: String -> String -> IO String promptTwoLines prompt1 prompt2 = do line1 <- promptLine prompt1 -- line1 :: String line2 <- promptLine prompt2 -- line2 :: String line1 ++ " and " ++ line2 -- ??
ここで問題発生です。これまでで、2行の入力行を読み込んでそれを結合する方法はわかっていますが、どうやって結合された文字列を返したらいいのか分かりません。doブロックはアクションをつなげて、その結果は最後のアクションの結果となることを思い出してください。
これでヘルパー関数を完成させることができます:
promptTwoLines :: String -> String -> IO String promptTwoLines prompt1 prompt2 = do line1 <- promptLine prompt1 -- line1 :: String line2 <- promptLine prompt2 -- line2 :: String return (line1 ++ " and " ++ line2) main :: IO () main = do both <- promptTwoLines "First line: " "Second line: " putStrLn ("you said " ++ both)
ここで、多くの初心者が混乱してしまう、非常に重要な点を押さえておきましょう。それは"return" はプログラムの制御フローには営業をおよぼさ"ない"ということです! returnはdoブロックの実行を"中断しません"。returnはよくdoブロックの途中で使われますが、それが必ずしもそのdoブロックの結果に関係があるというわけでもありません。returnというのは単なる関数で、特定の値を結果とするアクションを生成するというだけのことです。言い換えれば、値をアクションに"ラップしている"ということです。
if-then-elseやcase-ofあるいはアクションの再帰のようなHaskellの制御フローを使うこともできます。たとえば:
main :: IO () main = do line <- promptLine "What do you want? " -- line :: String if line == "wisdom" then putStrLn "No man is without enemies." else putStrLn ("I don't have any " ++ line)
main :: IO () main = do line <- promptLine "What do you want? " -- line :: String if line == "wisdom" then putStrLn "No man is without enemies." else do putStrLn ("I don't have any " ++ line) putStrLn "Perhaps you want some wisdom?"
let束縛もdoブロック内で用いることができます。このような形です:
main :: IO () main = do line <- promptLine "Enter a value: " -- line :: String let line2 = "\"" ++ line ++ "\"" in do -- line2 :: String putStrLn ("you said " ++ line2) putStrLn "Bye."
main :: IO () main = do line <- promptLine "Enter a value: " -- line :: String let line2 = "\"" ++ line ++ "\"" -- line2 :: String putStrLn ("you said " ++ line2) putStrLn "Bye."
let束縛とdo束縛はdoブロック内で自由に使うことができます。
2 逃げられません
最後に一つ、ぜひ知っておくべきIOアクションの詳細について紹介します。
それはアクションには逃げ道がない!ということです。つまりIOアクションから結果を得たければ、(こう言い切ってはしまいましたが、ここで告白することがあります。たぶんあとで気づくことになりますが、実はこれは一部間違いがあります。非常にまれな状況で、IOから脱出することができるのですが、その方法を使わない方が良い理由が多いのでここでは上のように結論づけました。
3 まとめ
- IOアクションはプログラムの外の世界に影響を与えるために使われる。
- アクションは引数はとらないが、結果の値は持つ。
- アクションは実行されるまで不活性なまま。Haskellプログラムの中ではたった1つのIOアクション()だけが実行される。main
- doブロックは複数のアクションを1つのアクションにまとめる。
- まとめられたIOアクションは副作用が観測できる形で順番に実行される。
- 矢印はdoブロック内でアクションの結果を束縛するために使われる。
- returnはアクションを作る関数である。制御フローの形式では"ない”。
Languages: en
