Haskell Quiz No.6

難易度: λλ

以下の Conduit を使ったコードの実行結果を予想してみてください!

答えは次回

最近は Conduit にはまっているので、クイズも Conduit が続きます。

はじめに

前回の問題と答えは以下の通りです。

問題

難易度: λλ

以下の Conduit を使ったコードの実行結果を予想してみてください!

こたえ

実際に実行してみましょう!

$ ./Quiz5.hs
("12345",40)

どうですか?予想通りでしたか??

Haskell Quiz No.5 の解説

この問題を解くためには conduit というストリーム処理ライブラリの知識が必要になります。

Conduit を使うモチベーション

具体例として指定したディレクトリ以下のファイル数容量の合計を出力するようなプログラムを作ってみましょう。

ディレクトリ操作については directory パッケージに便利な関数が色々と定義されているので、このパッケージを利用します。

必要な操作と、対応する関数は以下の通りです。

これらの関数を使って、こんな感じでプログラムを作ることができます。

実際に、プロファイリングを取得しつつ動かしてみます。

$ stack ghc Ex && sudo ./Ex /home/bm12/Desktop/ +RTS -s
総ファイル数: 338866
総ファイルサイズ: 37870090712

とりあえず、上手く動いているような気がします。

しかし、メモリ使用量は・・・

  13,440,124,048 bytes allocated in the heap
   8,760,418,592 bytes copied during GC
   1,225,650,008 bytes maximum residency (23 sample(s))
      19,423,400 bytes maximum slop
            2599 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0      9869 colls,     0 par    7.821s   9.831s     0.0010s    1.1223s
  Gen  1        23 colls,     0 par    0.011s   0.013s     0.0006s    0.0009s

  INIT    time    0.000s  (  0.000s elapsed)
  MUT     time    5.255s  (  6.347s elapsed)
  GC      time    7.832s  (  9.845s elapsed)
  EXIT    time    0.032s  (  0.123s elapsed)
  Total   time   13.118s  ( 16.315s elapsed)

  %GC     time      59.7%  (60.3% elapsed)

  Alloc rate    2,557,607,298 bytes per MUT second

  Productivity  40.3% of total user, 39.7% of total elapsed
  • 2599 MB total memory in use
  • %GC time 59.7% (60.3% elapsed)

ということで、非常にやばいですね。

Conduit で書き直そう!

先程作ったプログラムは、どうやらスペースリークしているようです。指定したディレクトリ以下のファイルの数とファイルサイズの合計を取得するだけなのに、メモリを使いすぎですね。

解決方法は色々ありますが、今回はストリームライブラリの Conduit を使って解決していきましょう。

Conduit には sourceDirectoryDeep という、関数が用意されています。

だいたいこんな感じで書き直すことができます。先程の定義と比べると sourceDirectoryDeep 関数のおかげでスッキリした印象です。

では、同様にプロファイルを取得しつつ、実行してみましょう。

$ stack ghc Ex2 && sudo ./Ex2 /home/bm12/Desktop/ +RTS -s
総ファイル数: 338866
総ファイルサイズ: 37870092264

肝心のメモリ使用量はと言うと・・・

  10,742,224,392 bytes allocated in the heap
      86,720,088 bytes copied during GC
          87,576 bytes maximum residency (19 sample(s))
          33,320 bytes maximum slop
               3 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0     10347 colls,     0 par    0.146s   0.198s     0.0000s    0.0008s
  Gen  1        19 colls,     0 par    0.000s   0.001s     0.0000s    0.0001s

  INIT    time    0.000s  (  0.000s elapsed)
  MUT     time    5.252s  (  7.444s elapsed)
  GC      time    0.146s  (  0.198s elapsed)
  EXIT    time    0.000s  (  0.000s elapsed)
  Total   time    5.398s  (  7.642s elapsed)

  %GC     time       2.7%  (2.6% elapsed)

  Alloc rate    2,045,428,118 bytes per MUT second

  Productivity  97.3% of total user, 97.4% of total elapsed
  • 3 MB total memory in use
  • %GC time 2.7% (2.6% elapsed)

どうですか?ストリーム処理って凄いですよね。

解説

この問題の重要なポイントは、実行すると ("12345", 6+7+8+9+10) という結果のように、[1..10] のリストの前半と後半で異なる処理になっているという点です。

$ ./Quiz5.hs
("12345",40)

ここで理解しておきたい知識は以下の3点です。

  • データはパイプ (ストリーム) を流れて処理されます
  • yieldMany 関数は受け取ったデータをパイプに流す準備をします (yieldMany は自分から積極的にデータを流すことはしません。準備だけしておき await 関数などで、実際に必要になった際にだけデータを流します)
  • .| はパイプを合成します

sink 関数は takeC 5 .| mapC show .| foldC というパイプと sumC というパイプからなる、大きなパイプです。

takeC 5 .| mapC show .| foldC 関数は takeC 5 の部分でデータを 5つだけ 上流のパイプに要求します。そのため、残りの5つのデータは次の sumC に流れることになります。

そのため、最終的には ("12345",40) となりました。

"12345" はそれぞれの Int 型が mapC show によって String 型に変換され、foldCmappend による畳込みによって文字列連結されます。

まとめ

実用的なアプリケーションを作ろうと考えている方は Conduit などのストリームライブラリを理解していると、色々と面倒なことを考えなくて済むのでとても良いですよ。

以上です。