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

From HaskellWiki
Revision as of 04:28, 19 December 2009 by Ymotongpoo (talk | contribs)
Jump to navigation Jump to search
The printable version is no longer supported and may have rendering errors. Please update your browser bookmarks and please use the default browser print function instead.

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)

This example uses the action getLine (getLine :: IO String) which reads a line of input from the console. The do-block makes an action that, when invoked, invokes the getLine, takes its result and invokes the action putStrLn ("you said: " ++ line) with the previous result bound to line.

Notice that an arrow (<-) is used in the binding and not an equal sign (as is done when binding with let or where). The arrow indicates that the result of an action is being bound. The type of getLine is IO String, and the arrow binds the result of the action to line which will be of type String.

We've used do-blocks to combine two actions together. This provides enough power to combine more actions together:

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

Since the innermost do-block is an action, it can be combined with getLine to make another action, which can be combined with putStrLn "Enter two lines" to make another more complicated action. Luckily we don't have to go through all this trouble. Do-blocks allow multiple actions to be specified in a single block. The meaning of these multi-action blocks is identical to the nested example above: the bindings are made visible to all successive actions. The previous example can be rewritten more compactly as

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

Of course we are free to use other Haskell language features when writing our program. Instead of putting all of our actions in main we may want to factor some common operations out as separate actions or functions that build actions. For example, we may want to combine prompting and user input:

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)

Here we made a function promptLine which returns an action. The action prints a prompt (using putStr :: IO (), which prints a string without a newline character) and reads a line from the console. The result of the action is the result of the last action, getLine.

Let's try to write a slightly more helper function that reads two lines and returns both of them concatenated together:

promptTwoLines :: String -> String -> IO String
promptTwoLines prompt1 prompt2 = do
    line1 <- promptLine prompt1                         -- line1 :: String  
    line2 <- promptLine prompt2                         -- line2 :: String
    line1 ++ " and " ++ line2    -- ??

There's a problem here. We know how to prompt for and read in both lines of input, and we know how to combine those lines of input, but we don't have an action that results in the combined string. Remember, do-blocks combine together actions and the result of the do-block is the result of the last action. line1 ++ " and " ++ line2 is a string, not an action resulting in a string and so cannot be used as the last line of the do-block. What we need is a way to make an action that results in a particular value. This is exactly what the return function does. Return is a function that takes any type of value and makes an action that results in that value. We can now complete our helper:

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)

In this example return (line1 ++ " and " ++ line2) is an action of type IO String that doesn't affect the outside world in any way, but results in a string that combines line1 and line2.

Here's a very important point that many beginners get confused about: "return" does not affect the control flow of the program! Return does not break the execution of the do-block. Return may occasionally be used in the middle of a do-block where it doesn't directly contribute to the result of the do-block. Return is simply a function that makes an action whose result is a particular value. In a sense it wraps up a value into an action.

We can also use Haskell's control flow features such as if-then-else, case-of or recursion with actions. For example:

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)

Recall that if chooses between two alternative values based on a boolean value. Here we used if to choose between two actions based on the result of the promptline "What do you want? " action. The result of the if is an action which the do-block combines into a larger action. If we want several actions to be performed within a then or else clause, we have to combine them into a single action using another do-block:

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-bindings are also available within do-blocks, for example:

main :: IO ()
main = do
    line <- promptLine "Enter a value: "                -- line :: String
    let line2 = "\"" ++ line ++ "\"" in do              -- line2 :: String
        putStrLn ("you said " ++ line2)
        putStrLn "Bye."

When working in do-blocks there is a more convenient syntax for using let which does not require the in keyword or any nesting. When using this syntax, the scope of the bound variable includes the remainder of the do-block, just as the scope of do-bindings (using arrows) within the do-block. The previous example can be written more compactly as:

main :: IO ()
main = do
    line <- promptLine "Enter a value: "                -- line :: String
    let line2 = "\"" ++ line ++ "\""                    -- line2 :: String
    putStrLn ("you said " ++ line2)
    putStrLn "Bye."

Let-bindings and do-bindings can be freely interspersed within the do-block.


もう逃げられません

There's one final detail about IO actions that you should be aware of: there is no escape! The only way to get a result from an IO action is to invoke the IO action (through main) and have its result used to affect the outside world through another IO action. There is no way to take an IO action and extract just its results to a simple value (an inverse-return). The only places where an IO action's results appear unwrapped are within a do-block.

Having said that, I have a confession to make: you will probably find out later that this is not entirely true. There are some rare exceptions that can be used in extreme circumstances to escape from IO, but there are good reasons why you will not be using them.


まとめ

  • IO actions are used to affect the world outside of the program.
  • Actions take no arguments but have a result value.
  • Actions are inert until run. Only one IO action in a Haskell program is run (main).
  • Do-blocks combine multiple actions together into a single action.
  • Combined IO actions are executed sequentially with observable side-effects.
  • Arrows are used to bind action results in a do-block.
  • Return is a function that builds actions. It is not a form of control flow!

Languages: en