遅延IO
Haskellを勉強したてのころ、interact*1という関数にひどくおどろかされたものである。
たとえば、
main = interact $ map toUpper
などと書けば入力を大文字に変換して出力するプログラムとなる。
こう書くといたって普通なのであるが、なんとこれ、遅延するのである。
つまり、入力が確定した時点で出力が少しづつ出てくるのである。
WinHugsならまさに一文字づつ大文字に変換されながら出てくるし、
GHCiでも一ラインづつ出てくる。
このときは遅延評価すげー、とかそのぐらいにしか考えていなかった。
しかしちょっと待てよ、これは遅延評価処理系ならば当然のことなのだろうか?
ここでIOモナドを考えてみる。
IOモナドにおいてその要素は(副作用を伴うかもしれない)計算、
bind演算は逐次実行を意味する。
つまり、m1 >> m2 と書けば、m1が実行され、その次にm2が
実行されるという意味になる。
さらに、モナドは代数の半群が元になっており、この演算には結合法則が
成り立っていなければならない。
つまり、(m1 >> m2) >> m3 は m1 >> (m2 >> m3) と等価である。
このことから考えると、m1 >> m2 と書くと、m1、m2の順に
計算が実行されるだけではなく、いかなる計算もm1、m2間には
割り込めないということになる。
で、interactを再度考える。
interactは入力を引っこ抜いてそれを渡された関数を通して出力
する関数になるので、ふつうに考えると
interact f = getContents >>= putStr . f
のように実装できる。
確かにこのように実装しても最初のコードはちゃんと遅延してくれるので
これは問題無さそうである。
次にgetContentsを考えてみる。
これは
getContents = do c <- getChar cs <- getContents return (c:cs)
あるいはリフト関数を用いて
getContents = liftM2 (:) getChar getContents
のように実装できるはずである。
(EOFの検知は実装してないけど)
で、このように実装したところ、最初のコードは遅延しなくなってしまった。
それもそのはずである。getCharの直後にgetContentsが実行されるので
その途中に割り込んでputStrが実行されるはずは無いのである。
じゃあ、遅延する関数が書けるのかというと、無理そうである。
IO Stringの型を持つ関数で、その返す値が遅延ストリームに
なるなんて、モナド則を破ってしまっているではないか。
ここにきて全くわからなくなってしまった。
Hugsのライブラリのソースを見てみると、
なんとgetContentsはprimitive扱いになっていた。
これはなんか悪いことしてるんと違うかなと思い
色々探していると
http://www.haskell.org/pipermail/haskell/2003-October/012895.html
このようなページを発見した。
どうやら同じことで悩んでいる人がいたようである。
で、それに対する答えは非常に明快であった。
unsafeInterleaveIO
これを使うというのである。
unsafeInterleaveIO は IO a -> IO a の型を持つ関数で、
これを通すと必要になるまで計算されない計算が作れる。
これを用いてgetContentsは
getContents = unsafeInterleaveIO $ liftM2 (:) getChar getContents
と定義することが出来る。
モナド則を破ってしまっているgetContentsは
やはり普通には実装できなかったのである。
でも、unsafeって…?
なんか気持ち悪い…?
しかし、これはそれもそのはずである。
一般に副作用を伴う計算が遅延してしまうと良くないのである。
詳しくはSICP*2あたりを読んでもらうとして、
たとえば、変数に書き込むなどといった処理を
好き勝手なタイミングで実行されると収集がつかなくなってしまう
というのは直感的に理解してもらえると思う。
しかし、getContentsなどといった一部の関数に対しては
その限りではない。表示に対して入力は人間が用意するものであるが、
むしろ実行タイミングによって人間が表示に反応して
入力を行うのを期待するわけである。(以前に書いた数当てゲームを参照)
(あと、入力が遅延することによって、入力が大きいなどの場合には
メモリ消費量などの点で有利となる)
というわけで、unsafeInterleaveIOなどというちょっと危ない関数が
有るとわかったのであるが、このunsafeInterleaveIO、
unsafeInterleaveIO = return . unsafePerformIO
このように定義できるのである。
unsafePerformIO はIO a -> aという型を持つ関数で、
なんと外せない筈のIOモナドがはずせてしまう。まさにunsafe。
要するに、いつの間にやら計算が実行されているのである。
Haskellの多くの初学者が犯す誤り(?)、
main = putChar getChar
これも
main = putChar $ unsafePerformIO getChar
unsafePerformIOでこの通りである。
もう、ほとんど反則であるが、かろうじて参照透明性だけは保っている
ような感じなのか。
ちなみに、unsafePerformIO をつかうと
gver = unsafePerformIO $ newIORef 0 main = do fact n ans <- readIORef gver print ans fact n = writeIORef gver $ product [1..n]