憎らしの Lazy I/O
このように I/O を分離し,モジュラリティを確保できるのは,Lazy I/O のお陰です.最近ではすっかりLazy I/O は悪者あつかいでいじめられっこになっていますが,こういう良いところもあるんです.
嗚呼,愛しのLazy I/O
コマッタちゃん Lazy I/O
たとえば,整数の入力を得る getInt :: IO Int を以下のように定義したとしましょう.
code:haskell
getInt :: IO Int
getInt = read <$> getLine
これを使って以下のようなプログラムを書きます.
code:haskell
main :: IO ()
main = do
{ a <- getInt
; b <- getInt
; print (a - b)
}
このプログラムは,標準入力から1行ずつ1つの整数を読み込み,先に入力された整数から,後に入力された整数を引いた結果を表示します.
実際にプログラムを起動してみると,
code:ghci
main
7
5
2
となります.予想どおりです.
Lazy I/O でなければ,do構文でI/Oアクションの出現順がそのまま入出力順になるので,プログラムの振る舞いを予想可能です.
また,入出力順を制御したければ,期待する順でI/Oアクションをならべて書けば,それが順次実行されると考えてよいわけです.
さて,この getIntが Lazy I/O だったらどうなると思いますか.
まず getInt の Lazy I/O 版 getInt' :: IO Int を定義しましょう.
code:haskell
getInt' :: IO Int
getInt' = unsafeInterleaveIO getInt
さて,これを使ってさきほどと同じようなプログラムを書いてみましょう.
code:haskell
main :: IO ()
main = do
{ a <- getInt'
; b <- getInt'
; print (a - b)
}
こちらも動作させてみましょう.
code:ghci
main
7
5
2
同じですね.なにも違いません.
では Lazy I/O の何がコマッタちゃんなんでしょう.
もう一度 Lazy I/O の振る舞いについて確認しましょう.
Lazy I/O に期待するのは要求駆動(demand-driven)の入出力です.
ということは,要求順に入出力がおこるということです.
2つめのプログラムの振る舞いを順に考えると,
1. このプログラムを完了するためには,print (a - b) を行わなければなりません.
2. print (a - b)を行うためには,a - bの値を知らなければなりません.
3. 式 a - bの値を知るためには,aとbの両方の値を知る必要があります.
4. もし,(-)が左のオペランドの値から要求するなら,aを束縛する入力から起こります.
5. すなわち,先の入力によってaが束縛され,後の入力によってbが束縛されますので,1つめのプログラムと同じ振る舞いになります.
実際そのようになっているということは,(-)は左オペランドの値を先に要求しているとも考えられます.
そこで,右オペランドの値を先に要求する (-.)を定義して実験してみましょう.
code:haskell
(-.) :: Int -> Int -> Int
(-.) = strictR (-)
strictR :: (a -> b -> c) -> (a -> b -> c)
strictR f x !y = f x y
! はバンパターン(bang pattern)注釈で,言語拡張BangPatternsを有効にすると使えるようになります.
これで,右オペランドの値を先に要求する(-.)を手にいれましたので,プログラムを
code:haskell
main :: IO ()
main = do
{ a <- getInt'
; b <- getInt'
; print (a -. b)
}
に変更して起動してみましょう.
code:ghci
main
7
5
-2
挙動が変りました.
1. このプログラムを完了するためには,print (a -. b)を行わなければなりません.
2. print (a -. b)を行うためには,a -. bの値を知らなければなりません.
3. 式a -. bの値を知るためには,aとbの両方の値を知る必要があります.
4. (-.)は右のオペランドの値から要求するので,bを束縛する入力から起こります.
5. すなわち,先の入力によってbが束縛され,後の入力によってaが束縛されますので上のような振る舞いになります.
lazy I/O の場合は do構文を使ってアクションを並べても,順次実行されるとは限らないのです.
得られるはずの値に対する要求順に I/O が生起するので,要求順が明らかでないと,プログラムの振る舞いを予想できません.
自分が定義した単純な関数なら要求順も判るかもしれませんが,ふつうは要求順は判らないものです.
というわけで,Lazy I/O のコマッタちゃんポイントは,
1. do構文の構成から予想されるプログラムの振る舞いと,実際のプログラムの振る舞いが異なる
2. I/Oで得られた結果の値で束縛される変数がどのタイミングで参照され値を要求されるかは判らない
ということです.
Imperativeスタイル v.s. Functionalスタイル
前節の1.の問題はimperativeな構文とプログラムの振る舞いの齟齬です.
では functionalに書けばよいのでしょうか.
code:haskell
main :: IO ()
main = print =<< (-.) <$> getInt' <*> getInt'
同じプログラムを functional スタイルで書いたものですが,やはり,どちらの getInt'が先に「実行される」かが問題になります.
ここでは,高階関数strictLやstrictRを明示的に使いましょう.
code:haskell
strictL, strictR :: (a -> b -> c) -> a -> b -> c
strictL f !x y = f x y
strictR f x !y = f x y
を用意しておいて,たとえば,
code:haskell
main :: IO ()
main = print =<< strictR (-) <$> getInt' <*> getInt'
とすれば,これは2つのgetInt'のうち,右側の I/O から実行さることが明示されたので,前節の問題は解決されましたね.