遅延IOは可能か

PyTestさんの記事を読んで考え込んでいました。取り上げられていた問題は、ディレクトリを深さ優先探索するコードなわけですが、これをpay as you goでやりたいときにはHaskellではどう書けばよいかというものです。
pay as you goで何がいいたいかというと、ディレクトリの探索はデータが必要になるまで遅らせたいということです。まさにHaskellのリストなんかで有効な遅延評価そのものですね…

ただ引っかかってくるのはディレクトリの内容を取得する関数も含めてファイルシステム関係の関数は全てIOな関数なことです。最初に僕が思ったことは次のとおりでした:

walk :: Path -> IO [Path]のシグネチャでコードを書くと遅延評価はできない。

unsafePerformIOを使えばできるだろうけど、何せunsafeなんだからできたら使わないで行きたい。

walkmapM_:: (Path -> IO a) -> Path -> IO ()みたいな風にして探索中にマップしてしまう関数にすれば遅延評価ではないまでも、pay as you goにはなる…けど、探索の中断をサポートするには関数パラメタに少し工夫をするか例外を使うかしなくてはならなさそう。

ひょっとしたらリストとIOを合成したモナドを作ればなんかうまくいくかも?なんていうのも思ったのですが、色々やってみて得た結論は、内部でどんなことをやろうとしても提供する関数のシグネチャがPath -> IO [Path]になっている時点で遅延評価はしないと宣言しているようなものだということです。
Path -> IO [Path]を翻訳すると、この関数はPathをとってIO [Path]を返す関数で、関数の帰り値は[Path]ではなくて、[Path]が帰ってくるIOな「計算」なわけです。そして、そのIOな「計算」をした後に残るのは[Path]、つまりそれはその関数が返せる全ての値を含んだリストでなくてはいけないわけです。

あきらめてunsafePerformIOで書いたのがこれ:

childDir :: FilePath -> IO [FilePath]
childDir path = getDirectoryContents path >>= 
  filterM (cur) >>= 
  mapM (return.absolute) >>= 
  filterM (isDir)
  where
    cur "." = return False
    cur ".." = return False
    cur _ = return True
    absolute :: FilePath -> FilePath
    absolute child = path  child

isDir :: FilePath -> IO Bool
isDir "."   = return False
isDir ".."  = return False
isDir child = doesDirectoryExist child

dirWalk :: FilePath -> [FilePath]
dirWalk path = path : (concatMap (dirWalk) 
  (unsafePerformIO $ childDir path))

ということで、dirWalkのシグネチャからはIOが消えました。動かしてみるとあっさり遅延モードで動きます。PyTestさんが言うようにこうゆうコードが普通にかけないと言語としてちょっとつらいものがありますね…こんなことを考えながら思い出されたのがPrelude.getContentsやSystem.IO.hGetContentsです。ライブラリの記述を見るとこの関数はファイルの中身をリストとして取り出すわけですが、シグネチャ
hGetContents :: Handle -> IO String
なくせに、ファイルアクセスは遅延だと書いてあります。しかしながら更に読み進んでいくと,
なにやらハンドルがsemi-closed状態になるだとか、なんだかとても怪しいことが書いてあります。コードを眺めてみると、このひとも、unsafeInterleaveIOなる関数を使っています。なんと…

どうやらunsafeInterleaveIOはIOな計算を遅延するためにある関数のようです。今回の用途にも使えそうです…

ここでunsafeな関数を使うことのunsafeさは一体どんなものがあるのでしょう?

IOモナドが必要になった根源的な理由は、僕の理解する範囲ではコードの実行順序に関係するものです。純粋関数型言語では値が必要になるまではその値を求める計算をしないわけですから、実行順序を強制したいときには何か特別なことをしてやらなくてはなりません。それを成し遂げるために導入されたのがモナドだったわけです…概念的にはIOは「外界」をパラメタに取る計算という説明もできるわけですが、実際に「外界」がパラメタとして関数に渡されているかというと、現実問題そうゆうこともないわけで…

実行順序に関して言えば、前々から思っていたのですが、IOな関数でなくてもリストに関する処理にも実行順序が決定しているものがあります。リストはその構造から言って、先頭からたどっていくしかないわけですから、例えば:

 map (+1) [1..5]

なんていうコードは、リストを先頭からトラバースしていきますね…最も(+1)がリストの各要素に適用される順序はもうちょっとあやふやですが…リストもモナドな型なわけなのですが、リストのモナドメソッドの振る舞いはこのリストの生成順序決定性とは関係ないことを記述しているように思います。

hGetContentsにしても、今回のdirWalkにしてもIOなコードをリストに埋め込んでいるので、関数の実行をして得られるリストの要素順に影響が出るようなコードを書くことはできなさそうです。

dirWalkの場合はトラバース中にディレクトリが削除されたり、新しいディレクトリができたりすると混乱するわけですが、これはファイルシステムをロックすることができない限り、遅延評価でなくても直面する問題ですね…

僕の今の理解では手が届かないのですが、Haskellでdo文がモナドに直結しているというところに多少の不自由さを感じます。ここは新しい型クラスSequentialを定義して、モナドは全てSequentialになるようにして、do文はSequentialとつなげるとかやるとかっこいい気もします。僕のイメージの中での話ですが…そすることで、モナドではないけど順序性が定義される型に対してdo文を使うことができるようになったらいいなと…まぁ、自分でも何言ってんだかわからないわけですが…

この辺のことがわかるまでは、unsafeは気をつけて使ったほうがいいけど、使うときは割り切って使うということが今回の教訓かもしれません…つまり、

遅延IOは可能かと聞かれれば、今のところの答えは「unsafeな関数を使えば」という答えになります。

あと、Windowsの空白文字を名前に含むディレクトリがあるとこのコードが動かないのがどうやったら解決できるのかがわかりませんでした…


ではでは。