IO入門編
(このページはHaskellではIOがどのように扱われているかを手っ取り早く紹介することを目的としています。学ぶべきことすべてはお伝えできませんが、どのように動作しているかを感覚はつかめると思います。)
Haskellでは、副作用は特定の型の値としてエンコードすることで考慮されなければならない、とすることで副作用がある処理と純粋な関数を切り離してきました。(IO a)
型の値はアクションです。これは実行されたらa
という型の値を生成しますよ、ということを表しています。
いくつか例を見てみましょう:
getLine :: IO String
putStrLn :: String -> IO () -- note that the result value is an empty tuple.
randomRIO :: (Random a) => (a,a) -> IO a -- in practice you will often avoid IO and prefer randomR
ふつうHaskellの評価ではこのような処理は起き得ません。(IO a)
型の値はほぼ実行されないまま残ります。実際、コンパイルされたHaskellプログラム中で本当に実行されるように命令されるIOアクションはmain
だけです。
このような前提を踏まえて、"hello, world"プログラムは次のように書けます。
main :: IO ()
main = putStrLn "Hello, World!"
ここまでいい感じにきていますが、エンド・トゥ・エンドのチェイン・アクションなしでは多くのことはできません。なので、HaskellではIOアクションを繋げる2、3の原則を提示しています。 そのうちの1つは以下の例で表されます。
(>>) :: IO a -> IO b -> IO b
もしx
と y
がIOアクションなら、(x >> y)
はx
を実行し、結果を破棄し、そのあとy
を実行してその結果を返します。
すごい、これでもう複数の手続きがあるプログラムが書けてしまいます。
main = putStrLn "Hello" >> putStrLn "World"
このコードは"Hello"と"World"を別々の行に表示します。
しかしながら、まだ最初の結果を用いて次の結果が何をするか決めるようなチェイン・アクションの書き方が分かっていません。これは次に示すような 'バインド(束縛)' と呼ばれる操作で実現できます。
(>>=) :: IO a -> (a -> IO b) -> IO b
ここでx >>= f
は最初にアクションx
を実行して、その結果をつかんで、f
に渡してそれが次のアクションが実行される、という一連のアクションを表しています。一連のアクションが実行されると、その結果は全体の計算結果となります。
説明が長くなってしまいましたが、百聞は一見にしかずで、使用例を見ればすぐにわかると思います。
main = putStrLn "Hello, what is your name?"
>> getLine
>>= \name -> putStrLn ("Hello, " ++ name ++ "!")
これこそ必要としていたものでした。実際この束縛関数は非常によくできていて、先ほどの(>>)
も次のように定義できます。
x >> y = x >>= const y
実際に、ある値をなにもしないでそのまま値を返すようなIOアクションに変換することも非常に大事だとわかりました。これはチェイン・アクションの最後のほうで使うと非常に便利です。これによって、チェインの最後のアクションに処理をゆだねるのではなく、自分たちでどう処理して何を返すか決定することができます。より原始的には
return :: a -> IO a
と書け、いま言ったようなことができます。
do記法はあらゆるHaskellプログラムで見ることができます。do記法を用いた場合、先ほどの例は次のようになります:
main = do putStrLn "Hello, what is your name?"
name <- getLine
putStrLn ("Hello, " ++ name ++ "!")
do記法を使った例は実際にdo記法を使わない例と全く同じことであり、Haskellコンパイラによってdo記法を同じものにコンパイルされる。なので、doブロックを見たら、(>>)
と (>>=)
の連鎖とアクションの結果を取得するのに適切なラムダ式を想像してみてください。
doブロック内の各行にある各アクションは普通に実行され、v <- x
となっている行ではアクションx
を実行し、結果を変数v
に割り当てます。
よくx
の位置にアクションでないものを書いてしまうミスをすることがあります。ありがちなのは値を書くことです。もしdoブロック内で変数への束縛をアクションなしに行いたい場合は、let a = b
という書き方をすればできます。これは一般的なlet式のようにa
をb
を同じものと定義しますが、doブロック外のスコープには適用されません。
次のような関数はないことに気を付けてください:
unsafe :: IO a -> a
理由は簡単で、これを許してしまうとHaskellの参照透過性が侵されてしまうからです。―たとえば、unsafe
を同じIOアクションに適用しても毎回異なる結果を返します。そしてこれはHaskellの関数にはあってはならない振る舞いです。
まだモナドについては一般的なことはほとんどお伝えできていません。ほとんどのモナドは実際はIOとは似ていませんが、バインドとreturnに関しては同じようなコンセプトを持っています。より一般的にモナドを学びたい場合は Monads as containers の記事か Monads as computationの記事を読んでみてください。2つの記事はそれぞれモナドを異なる側面から大まかに説明しています。
さらに読みたい方に
- IOモナドを用いたより理解しやすいチュートリアルを見たい方は Haskell I/O inside: Down the Rabbit's Hole を読んでみてください
- 基本的なモナド関数の説明と例はリファレンスガイド A tour of the Haskell Monad functionsにあります。(Henk-Jan van Tuyl著)
- さらにこちらも参照してみてください。 how to avoid IO
Languages: en