Haskell::OGR-nの探索(その7)

ファイルI/Oの手習いとして、その6’のコードに計算したOGR-nの結果を一通りテキストファイルに書き出す機能を追加。さらに、2回目以降に実行した場合既に計算結果があれば、それを元にOGR-nのリストを作り、途中から計算を始めるようにした。

ogr7.hs

後、2分探索の部分もちょこっとだけ最適化。

ファイル出力の部分はある程度定石を覚えれば、それほど悩むことはない。
問題は、ファイル入力->オブジェクトの生成のようなパターン。

Haskellの遅延I/Oの元ではテキストファイルの入力処理は

do
  inh <- openFile 〜 ReadMode
  cs <- hGetContents inh

  ごにょごにょ

  hClose inh

のような感じで書くことができる。csにはオープンしたファイルの内容がString型として束縛される。一見、ファイルが全てメモリ上にロードされてしまうように見えるが、遅延I/Oのお陰で評価に必要な部分しか読み出されない。
で、ごにょごにょしている部分でcsにいろいろな関数を適用して、必要な計算を行う。
ところが、このごにょごにょの部分も普通は遅延評価されてしまうので、hCloseまでに正格評価を行わないと、必要な評価の前にファイルハンドルがクローズされてしまい、csには空のStringしか束縛されていないということが起こる。

ごにょごにょの部分で作るオブジェクトが単純な値やリストであれば、seqなどの正格評価を摘要するための演算子を用いればいいのだが、複雑な composite object になるとかなり厄介。
そういう用途に、Control.Parallel.Strategyモジュールがあるのだが、このあたりは正直まだ理解不能な領域だったり。

普通にテキストフィルター的な(ファイルのオープン/クローズを気にしなくてもいいような)処理を記述する場合は、遅延I/Oは直観的にコードを書き下せるのでとても便利なんだが。
このあたりの迂遠さというか、正格/非正格評価の区別をプログラマに要求するあたりが、関数型言語板なんかでのHaskell嫌いな人の攻撃材料になってるんだろうなぁと思ったり思わなかったり。

結局、

writeFile “/dev/null” $ show ほにゃらら

みたいに、オブジェクトを一旦プリンタブルな文字列に変換して、出力プリミティブを適用するという、かなり滅茶滅茶なやりかたでその場をしのいでいる。

実際のところの計算時間は、その6’のn=13の計算を一旦キャンセルして再度やり直し中。
n=12まで完了していて、

3:([0,1,3],[0,2,3])
0.000u 0.001s 0:00.00 0.0%      0+0k 0+0io 0pf+0w
4:([0,1,4,6],[0,2,5,6])
0.000u 0.002s 0:00.00 0.0%      0+0k 0+0io 0pf+0w
5:([0,1,4,9,11],[0,2,7,10,11])
5:([0,2,7,8,11],[0,3,4,9,11])
0.000u 0.002s 0:00.00 0.0%      0+0k 0+0io 0pf+0w
6:([0,1,4,10,12,17],[0,5,7,13,16,17])
6:([0,1,4,10,15,17],[0,2,7,13,16,17])
6:([0,1,8,11,13,17],[0,4,6,9,16,17])
6:([0,1,8,12,14,17],[0,3,5,9,16,17])
0.000u 0.006s 0:00.00 0.0%      0+0k 0+0io 0pf+0w
7:([0,2,3,10,16,21,25],[0,4,9,15,22,23,25])
7:([0,1,4,10,18,23,25],[0,2,7,15,21,24,25])
7:([0,1,7,11,20,23,25],[0,2,5,14,18,24,25])
7:([0,1,11,16,19,23,25],[0,2,6,9,14,24,25])
7:([0,2,7,13,21,22,25],[0,3,4,12,18,23,25])
0.028u 0.001s 0:00.02 100.0%    1264+1048k 0+0io 0pf+0w
8:([0,1,4,9,15,22,32,34],[0,2,12,19,25,30,33,34])
0.346u 0.001s 0:00.34 100.0%    855+708k 0+0io 0pf+0w
9:([0,1,5,12,25,27,35,41,44],[0,3,9,17,19,32,39,43,44])
4.583u 0.022s 0:04.60 100.0%    840+697k 0+0io 0pf+0w
10:([0,1,6,10,23,26,34,41,53,55],[0,2,14,21,29,32,45,49,54,55])
83.079u 0.120s 1:23.22 99.9%    840+696k 0+0io 0pf+0w
11:([0,1,4,13,28,33,47,54,64,70,72],[0,2,8,18,25,39,44,59,68,71,72])
11:([0,1,9,19,24,31,52,56,58,69,72],[0,3,14,16,20,41,48,53,63,71,72])
1891.324u 1.534s 31:37.07 99.7% 840+696k 0+0io 0pf+0w
12:([0,2,6,24,29,40,43,55,68,75,76,85],[0,9,10,17,30,42,45,56,61,79,83,85])
7698.658u 9.628s 2:08:46.74 99.7%       840+696k 0+0io 0pf+0w

と、n=9以降のスピードアップに貢献してくれている。
n=13は現在計算中。

当初は、nの増加につれてGR-nの生成とは比べ物にならないほどの勢いで計算時間が増加していたので、あまりこの手法の導入に必要性を感じていなかった。
最近のコードの改良である程度実用的な時間で計算できるようになってきたので、計算過程を記録して途中から計算を始めることにもメリットが出てきたと思う。

なお、空でもいいからカレントディレクトリにogr.txt(結果が出力されるファイル)という名前のファイルがないとエラーになるのは、一応仕様です。