Difference between revisions of "Hakell I/Oアクションの紹介"

From HaskellWiki
Jump to: navigation, search
m (Assigned category based on output of automated translators...)
 
(3 intermediate revisions by one other user not shown)
Line 31: Line 31:
 
そういう意味で、putStrLnはアクションではないですが、<hask>putStrLn "hello"</hask>はアクションです。微妙な違いでしかないですが、重要なことです。すべてのIOアクションはある型aに対するIO a型を持っています。IOアクションではそれ以上の引数を絶対にとりませんが、アクションを作る関数(たとえば<hask>putStrLn</hask>)は引数をとります。
 
そういう意味で、putStrLnはアクションではないですが、<hask>putStrLn "hello"</hask>はアクションです。微妙な違いでしかないですが、重要なことです。すべてのIOアクションはある型aに対するIO a型を持っています。IOアクションではそれ以上の引数を絶対にとりませんが、アクションを作る関数(たとえば<hask>putStrLn</hask>)は引数をとります。
  
アクションというのは説明書のようなものです。アクションによって何ができるかを示しています。アクションそれ自身は何か処理を実行することはありませんが、”実行される" ことによって何かが起きます。単にアクションがあるだけでは何も起きないのです。例えばHaskellでは<hask>putStrLn "hello"</hask>は"hello"という行を表示するアクションです。型としては<hask>IO ()</hask>を持っています。たとえば次のような定義をもつHaskellプログラムを書いてみます。
+
アクションというのは説明書のようなものです。アクションによって何ができるかを示しています。アクションそれ自身は何か処理を実行することはありませんが、 "実行される" ことによって何かが起きます。単にアクションがあるだけでは何も起きないのです。例えばHaskellでは<hask>putStrLn "hello"</hask>は"hello"という行を表示するアクションです。型としては<hask>IO ()</hask>を持っています。たとえば次のような定義をもつHaskellプログラムを書いてみます。
 
<haskell>
 
<haskell>
 
x = putStrLn "hello"
 
x = putStrLn "hello"
Line 65: Line 65:
 
     putStrLn ("you said: " ++ line)
 
     putStrLn ("you said: " ++ line)
 
</haskell>
 
</haskell>
This example uses the action <hask>getLine</hask> (<hask>getLine :: IO String</hask>) which reads a line of input from the console.  The do-block
+
この例では<hask>getLine</hask><hask>getLine :: IO String</hask>)というアクションを使っています。このアクションはコンソールから入力行を読み取ります。doブロックは呼ばれたときに<hask>getLine<hask>を呼び出し、その結果を受け取って<hask>putStrLn ("you said: " ++ line)</hask>というアクションを先ほどの結果を<hask>line</hask>に束縛した形で呼び出します。
makes an action that, when invoked, invokes the <hask>getLine</hask>,
 
takes its result and invokes
 
the action <hask>putStrLn ("you said: " ++ line)</hask> with the previous
 
result bound to <hask>line</hask>.
 
  
Notice that
+
ここで等号ではなく、矢印(<hask><-</hask>)が束縛に用いられていることに注意してください。(<hask>let</hask><hask>where</hask>を使うときは等号を使います)
an arrow (<hask><-</hask>) is used in the binding and not an equal sign
+
矢印はアクションの結果がいままさに束縛されています、ということを表しています。<hask>getLine</hask>の型は<hask>IO String</hask>です。そして矢印はアクションの結果を<hask>line</hask>に束縛します。その型は<hask>String</hask>になります。
(as is done when binding with <hask>let</hask> or <hask>where</hask>).
 
The arrow indicates that the result of an action is being bound.  The
 
type of <hask>getLine</hask> is <hask>IO String</hask>, and the arrow
 
binds the result of the action to <hask>line</hask> which will be of
 
type <hask>String</hask>.
 
  
We've used do-blocks to combine two actions together.  This provides enough
+
これまでdoブロックを2つのアクションを繋げるために使ってきました。これによりもっと多くのアクションを繋ぐことも可能とわかります:
power to combine more actions together:
 
 
<haskell>
 
<haskell>
 
main :: IO ()
 
main :: IO ()
Line 91: Line 81:
 
             putStrLn ("you said: " ++ line1 ++ " and " ++ line2)
 
             putStrLn ("you said: " ++ line1 ++ " and " ++ line2)
 
</haskell>
 
</haskell>
Since the innermost do-block is an action, it can be combined with
+
最下層のdoブロックはアクションなので、<hask>getLine</hask>と他のアクションを一緒に繋げることができます。そして、<hask>getLine</hask><hask>putStrLn "Enter two lines"</hask>と繋げることができ、より複雑なアクションを作ることができます。
<hask>getLine</hask> to make another action, which can be combined with
+
幸いなことに、こんな面倒な書き方をする必要はありません。doブロックでは複数のアクションを1つのブロックに書き下すことができます。
<hask>putStrLn "Enter two lines"</hask> 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
 
 
<haskell>
 
<haskell>
 
main :: IO ()
 
main :: IO ()
Line 107: Line 93:
 
</haskell>
 
</haskell>
  
Of course we are free to use other Haskell language features when writing
+
もちろん、プログラムを書くときは他の書き方をしても一向に構いません。全てのアクションは<hask>main</hask>に書く代わりに、共通の操作を、別のアクションとしたり、アクションを生成する関数としたりして切り出したくなることもあるでしょう。たとえば、プロンプトの表示と、ユーザの入力をくっつけたくなった場合:
our program.  Instead of putting all of our actions in <hask>main</hask>
 
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:
 
 
<haskell>
 
<haskell>
 
promptLine :: String -> IO String
 
promptLine :: String -> IO String
Line 124: Line 106:
 
     putStrLn ("you said: " ++ line1 ++ " and " ++ line2)
 
     putStrLn ("you said: " ++ line1 ++ " and " ++ line2)
 
</haskell>
 
</haskell>
Here we made a function <hask>promptLine</hask> which returns an action.
+
ここでは<hask>promptLine</hask>という関数を作りました。この関数はアクションを返します。
The action prints a prompt (using <hask>putStr :: IO ()</hask>, which prints a
+
このアクションはプロンプトを表示して(<hask>putStr :: IO ()</hask>を使っています。これは文字列を改行せずに表示します)、コンソールから入力を読み込みます。
string without a newline character) and reads a line from the console.
+
このアクションの結果は最後のアクションの結果、つまり<hask>getLine</hask>となります。
The result of the action is the result of the last action, <hask>getLine</hask>.
 
  
Let's try to write a slightly more helper function that reads two lines
+
2行読みこんで、それらを結合して返すような便利な関数を書いてみましょう。
and returns both of them concatenated together:
 
 
<haskell>
 
<haskell>
 
promptTwoLines :: String -> String -> IO String
 
promptTwoLines :: String -> String -> IO String
Line 138: Line 118:
 
     line1 ++ " and " ++ line2    -- ??
 
     line1 ++ " and " ++ line2    -- ??
 
</haskell>
 
</haskell>
There's a problem here.  We know how to prompt for and read in both
+
ここで問題発生です。これまでで、2行の入力行を読み込んでそれを結合する方法はわかっていますが、どうやって結合された文字列を返したらいいのか分かりません。doブロックはアクションをつなげて、その結果は最後のアクションの結果となることを思い出してください。
lines of input, and we know how to combine those lines of input, but
+
<hask>line1 ++ " and " ++ line2</hask>は文字列でありアクションではないので、doブロックの最後に使うことはできません。
we don't have an action that results in the combined string.  Remember,
+
いまここで必要なのは、ある特定の値を結果とするアクションを作る方法です。これこそまさに<hask>return</hask>関数が行う内容です。returnはどのような型の値も引数にとって、そのような値を結果とするアクションを作る関数です。
do-blocks combine together actions and the result of the do-block
+
これでヘルパー関数を完成させることができます:
is the result of the last action.  <hask>line1 ++ " and " ++ line2</hask>
 
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 <hask>return</hask> 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:
 
 
<haskell>
 
<haskell>
 
promptTwoLines :: String -> String -> IO String
 
promptTwoLines :: String -> String -> IO String
Line 161: Line 134:
 
     putStrLn ("you said " ++ both)
 
     putStrLn ("you said " ++ both)
 
</haskell>
 
</haskell>
In this example <hask>return (line1 ++ " and " ++ line2)</hask> is an
+
この例では <hask>return (line1 ++ " and " ++ line2)</hask> はアクション型<hask>IO String</hask>であり、その型自身は外部世界に影響を与えることはないですが、これで<hask>line1</hask><hask>line2</hask>を結合した文字列を結果とすることができました。
action of type <hask>IO String</hask> that doesn't affect the outside world in any way, but results in a string that combines <hask>line1</hask> and <hask>line2</hask>.
 
  
Here's a very important point that many beginners get confused
+
ここで、多くの初心者が混乱してしまう、非常に重要な点を押さえておきましょう。それは"return" はプログラムの制御フローには営業をおよぼさ"ない"ということです!
about: "return" does ''not'' affect the control flow of the program!
+
returnはdoブロックの実行を"中断しません"。returnはよくdoブロックの途中で使われますが、それが必ずしもそのdoブロックの結果に関係があるというわけでもありません。returnというのは単なる関数で、特定の値を結果とするアクションを生成するというだけのことです。言い換えれば、値をアクションに"ラップしている"ということです。
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,
+
if-then-elseやcase-ofあるいはアクションの再帰のようなHaskellの制御フローを使うこともできます。たとえば:
case-of or recursion with actions.  For example:
 
 
<haskell>
 
<haskell>
 
main :: IO ()
 
main :: IO ()
Line 184: Line 149:
 
</haskell>
 
</haskell>
  
Recall that <hask>if</hask> chooses between two alternative values based on a
+
<hask>if</hask>が真偽値によって2つの値から1つを選ぶような制御だということを思い出してください。
boolean value.
+
ここでは<hask>if</hask><hask>promptLine "What do you want?"というアクションの結果にもとづいて2つのアクションから洗濯するような制御として使いました。この<hask>if</hask>の結果はdoブロックがより大きなアクションへと取り込みます。もし複数のアクションを<hask>then</hask>節や<hask>else</hask>節で実行したいと思ったら、さらに別のdoブロックを使ってそれらを1つのアクションにまとめなければいけません。
Here we used <hask>if</hask> to choose between two actions based on
 
the result of the <hask>promptline "What do you want? "</hask> action.
 
The result of the <hask>if</hask> is an action which the do-block
 
combines into a larger action.  If we want several actions to be performed
 
within a <hask>then</hask> or <hask>else</hask> clause, we have to
 
combine them into a single action using another do-block:
 
 
<haskell>
 
<haskell>
 
main :: IO ()
 
main :: IO ()
Line 203: Line 162:
 
</haskell>
 
</haskell>
  
Let-bindings are also available within do-blocks, for example:
+
let束縛もdoブロック内で用いることができます。このような形です:
 
<haskell>
 
<haskell>
 
main :: IO ()
 
main :: IO ()
Line 212: Line 171:
 
         putStrLn "Bye."
 
         putStrLn "Bye."
 
</haskell>
 
</haskell>
When working in do-blocks there is a more convenient syntax for using
+
doブロックを使っているときは、<hask>let</hask>を使うにあたってより便利な構文があります。<hask>in</hask>キーワードや入れ子を使わなくて良いというのがそれなのですが、この構文を使うときは、束縛された変数のスコープはdoブロック内の残りのアクションとなります。これは矢印を用いたdoブロック内でのdo束縛のスコープと同様です。先の例はより簡潔に書けます:
<hask>let</hask> which does not require the <hask>in</hask> 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:
 
 
<haskell>
 
<haskell>
 
main :: IO ()
 
main :: IO ()
Line 226: Line 180:
 
     putStrLn "Bye."
 
     putStrLn "Bye."
 
</haskell>
 
</haskell>
Let-bindings and do-bindings can be freely interspersed within the do-block.
+
let束縛とdo束縛はdoブロック内で自由に使うことができます。
  
 +
== 逃げられません ==
 +
最後に一つ、ぜひ知っておくべきIOアクションの詳細について紹介します。
 +
それはアクションには逃げ道がない!ということです。つまりIOアクションから結果を得たければ、(<hask>main</hask>を通じて)IOアクションを呼び起こして、その結果を他のIOアクションを通じて外部に影響を与えるしか方法はないのです。IOアクションから結果だけを取り出して値を得ること(逆<hask>return</hask>)はできないのです。IOアクションの結果が"ラップされていない状態で"存在できるのはdoブロックの中だけなのです。
  
== もう逃げられません ==
+
こう言い切ってはしまいましたが、ここで告白することがあります。たぶんあとで気づくことになりますが、実はこれは一部間違いがあります。非常にまれな状況で、IOから脱出することができるのですが、その方法を使わない方が良い理由が多いのでここでは上のように結論づけました。
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 <hask>main</hask>) 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-<hask>return</hask>).  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.
+
* IOアクションはプログラムの外の世界に影響を与えるために使われる。
* Actions take no arguments but have a result value.
+
* アクションは引数はとらないが、結果の値は持つ。
* Actions are inert until run.  Only one IO action in a Haskell program is run (<hask>main</hask>).
+
* アクションは実行されるまで不活性なまま。Haskellプログラムの中ではたった1つのIOアクション(<hask>main</hask>)だけが実行される。
* Do-blocks combine multiple actions together into a single action.
+
* doブロックは複数のアクションを1つのアクションにまとめる。
* Combined IO actions are executed sequentially with observable side-effects.
+
* まとめられたIOアクションは副作用が観測できる形で順番に実行される。
* Arrows are used to bind action results in a do-block.
+
* 矢印はdoブロック内でアクションの結果を束縛するために使われる。
* Return is a function that builds actions.  It is ''not'' a form of control flow!
+
* returnはアクションを作る関数である。制御フローの形式では"ない”。
  
 
Languages: [[Introduction to Haskell IO/Actions|en]]
 
Languages: [[Introduction to Haskell IO/Actions|en]]
 +
 +
[[Category:Jp]]

Latest revision as of 03:11, 9 April 2021

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