Hakell I/Oアクションの紹介

From HaskellWiki
Revision as of 15:00, 23 December 2009 by Ymotongpoo (talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Haskellでプログラミングするときに副作用があるような処理、あるいは外部に対して働きかけるような処理を書きたいときは アクション を使います。 アクションはHaskellの言語仕様の中では、3という数字や"hello world"という文字列、あるいはmapという関数と同様に値として扱われます。 つまり変数名に束縛したり、関数に引数として与えたり、関数の結果とすることが可能ということです。 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 ()

PutStrLn は引数をとりますが、アクションではありません。引数を1つ(文字列)とって、IO () というアクションの型を返す関数です。 そういう意味で、putStrLnはアクションではないですが、putStrLn "hello"はアクションです。微妙な違いでしかないですが、重要なことです。すべてのIOアクションはある型aに対するIO a型を持っています。IOアクションではそれ以上の引数を絶対にとりませんが、アクションを作る関数(たとえばputStrLn)は引数をとります。

アクションというのは説明書のようなものです。アクションによって何ができるかを示しています。アクションそれ自身は何か処理を実行することはありませんが、 "実行される" ことによって何かが起きます。単にアクションがあるだけでは何も起きないのです。例えばHaskellではputStrLn "hello"は"hello"という行を表示するアクションです。型としてはIO ()を持っています。たとえば次のような定義をもつHaskellプログラムを書いてみます。

x = putStrLn "hello"

しかしこれではプログラムに"hello"と表示させることはできないのです!Haskellは"main"と呼ばれるIOアクションしか実行できないことになっています。このアクションはIO ()という型を持っています。次のHaskellプログラムは"hello"を表示する"でしょう":

module Main where

main :: IO ()
main = putStrLn "hello"

注意してください

「じゃあ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ブロックの最後のアクションの結果となります。先ほどの例では、最後のアクション(putStrLn)は特に結果を生成しないため、doブロック全体の型はIO ()となります。

doブロックはあるアクションの結果を他のアクションの生成のために使うこともできます。たとえばこんな具合です:

main:: IO ()
main = do
    line <- getLine                                     -- line :: String
    putStrLn ("you said: " ++ line)

この例ではgetLinegetLine :: IO String)というアクションを使っています。このアクションはコンソールから入力行を読み取ります。doブロックは呼ばれたときにgetLine<hask>を呼び出し、その結果を受け取って<hask>putStrLn ("you said: " ++ line)というアクションを先ほどの結果をlineに束縛した形で呼び出します。

ここで等号ではなく、矢印(<-)が束縛に用いられていることに注意してください。(letwhereを使うときは等号を使います) 矢印はアクションの結果がいままさに束縛されています、ということを表しています。getLineの型はIO Stringです。そして矢印はアクションの結果をlineに束縛します。その型はStringになります。

これまで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ブロックはアクションなので、getLineと他のアクションを一緒に繋げることができます。そして、getLineputStrLn "Enter two lines"と繋げることができ、より複雑なアクションを作ることができます。 幸いなことに、こんな面倒な書き方をする必要はありません。doブロックでは複数のアクションを1つのブロックに書き下すことができます。 マルチアクション・ブロックの意味は前述の入れ子の例をみれば明らかだと思います。束縛はそれ以降の全てのアクションから観測可能です。先の例はこのようにコンパクトに書き直すことができます。

main :: IO ()
main = do
    putStrLn "Enter two lines"
    line1 <- getLine                                    -- line1 :: String
    line2 <- getLine                                    -- line2 :: String
    putStrLn ("you said: " ++ line1 ++ " and " ++ line2)

もちろん、プログラムを書くときは他の書き方をしても一向に構いません。全てのアクションはmainに書く代わりに、共通の操作を、別のアクションとしたり、アクションを生成する関数としたりして切り出したくなることもあるでしょう。たとえば、プロンプトの表示と、ユーザの入力をくっつけたくなった場合:

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)

ここではpromptLineという関数を作りました。この関数はアクションを返します。 このアクションはプロンプトを表示して(putStr :: IO ()を使っています。これは文字列を改行せずに表示します)、コンソールから入力を読み込みます。 このアクションの結果は最後のアクションの結果、つまりgetLineとなります。

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ブロックはアクションをつなげて、その結果は最後のアクションの結果となることを思い出してください。 line1 ++ " and " ++ line2は文字列でありアクションではないので、doブロックの最後に使うことはできません。 いまここで必要なのは、ある特定の値を結果とするアクションを作る方法です。これこそまさにreturn関数が行う内容です。returnはどのような型の値も引数にとって、そのような値を結果とするアクションを作る関数です。 これでヘルパー関数を完成させることができます:

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 (line1 ++ " and " ++ line2) はアクション型IO Stringであり、その型自身は外部世界に影響を与えることはないですが、これでline1line2を結合した文字列を結果とすることができました。

ここで、多くの初心者が混乱してしまう、非常に重要な点を押さえておきましょう。それは"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)

ifが真偽値によって2つの値から1つを選ぶような制御だということを思い出してください。 ここではifpromptLine "What do you want?"というアクションの結果にもとづいて2つのアクションから洗濯するような制御として使いました。この<hask>ifの結果はdoブロックがより大きなアクションへと取り込みます。もし複数のアクションをthen節やelse節で実行したいと思ったら、さらに別のdoブロックを使ってそれらを1つのアクションにまとめなければいけません。

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."

doブロックを使っているときは、letを使うにあたってより便利な構文があります。inキーワードや入れ子を使わなくて良いというのがそれなのですが、この構文を使うときは、束縛された変数のスコープはdoブロック内の残りのアクションとなります。これは矢印を用いたdoブロック内でのdo束縛のスコープと同様です。先の例はより簡潔に書けます:

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ブロック内で自由に使うことができます。

逃げられません

最後に一つ、ぜひ知っておくべきIOアクションの詳細について紹介します。 それはアクションには逃げ道がない!ということです。つまりIOアクションから結果を得たければ、(mainを通じて)IOアクションを呼び起こして、その結果を他のIOアクションを通じて外部に影響を与えるしか方法はないのです。IOアクションから結果だけを取り出して値を得ること(逆return)はできないのです。IOアクションの結果が"ラップされていない状態で"存在できるのはdoブロックの中だけなのです。

こう言い切ってはしまいましたが、ここで告白することがあります。たぶんあとで気づくことになりますが、実はこれは一部間違いがあります。非常にまれな状況で、IOから脱出することができるのですが、その方法を使わない方が良い理由が多いのでここでは上のように結論づけました。


まとめ

  • IOアクションはプログラムの外の世界に影響を与えるために使われる。
  • アクションは引数はとらないが、結果の値は持つ。
  • アクションは実行されるまで不活性なまま。Haskellプログラムの中ではたった1つのIOアクション(main)だけが実行される。
  • doブロックは複数のアクションを1つのアクションにまとめる。
  • まとめられたIOアクションは副作用が観測できる形で順番に実行される。
  • 矢印はdoブロック内でアクションの結果を束縛するために使われる。
  • returnはアクションを作る関数である。制御フローの形式では"ない”。

Languages: en