Conduit最速入門2019

メリークリスマス。
アドベントカレンダーの枠が埋まっていないので、ちょっとお邪魔してconduitをやります。
conduitはバージョン1.3系統で大きくAPIが変わっているのですが、日本語による新しい解説が見当たらないので、簡単にチュートリアルをやっていこうと思います。

---

Conduitは「読み込む→何かする→書き込む→繰り返し」みたいな処理をHaskellで書くためのライブラリ、またはフレームワークです。

本来、純粋なシーケンス相手であれば、こういう処理は得意なはずのHaskellなのですが、読み書きが入って来ると途端に話が複雑になります。

実質的な選択肢は3つ。

1. 普通にdo文で手続き型っぽく書く
2. Lazy IO
3. なんかフレームワークを使う

1は~~実は最善策という説がありますが~~、やめておきましょう。2はトイプログラムには良いのですが、実用的にはいろいろ問題がある事が分かっています。
というわけで、3の選択肢、conduitを使っていきます。

## 最速入門

`conduit` パッケージをお手元の環境に追加し、インポート文をぱぱっと書きます。

```haskell
import Data.Conduit
import qualified Data.Conduit.Combinators as C
```

[Data.Conduit.Combinators](http://hackage.haskell.org/package/conduit/docs/Data-Conduit-Combinators.html)モジュールからトランスデューサ(ストリームを構成する部品)を探してきて、 `.|` でつないで `runConduit` します。

``` haskell
main = runConduit (C.yieldMany [1..5] .| C.mapM_ putStrLn)
```

```
1
2
3
4
5
```

ね、簡単でしょ。[^1]

## もう少し詳しく
### ConduitTの型
Combinatorsから探してきた `yieldMany` や `mapM_` などのトランスデューサは、 `ConduitT` という型を持ちます。

```haskell
ConduitT i o m r
```

* i : 入力値
* o : 出力値
* m : ベースモナド
* r : 戻り値

出力値と戻り値があるんですね。
どう違うんでしょうか。

* 出力値 : `.|` でつないだとき後続のトランスデューサの入力になる
* 戻り値 : runConduitの戻り値になる

なるほど。
なので、 `sum` などのトランスデューサの結果を出すには `runConduit` の戻り値を見る必要があります。

```haskell
main = do
  r <- runConduit (C.yieldMany [1 .. 10] .| C.sum)
  print r -- => 55
```

なお `.|` でつないだとき、全体の戻り値は一番右のトランスデューサのものになります。[^2]


### 処理の終了と継続
今まで挙げたトランスデューサは、入力をすべて消費するまでループします。
一方、途中で `return` して止まるやつもあります。

`await` は、入力を1個だけ読んで止まります。

```haskell
main = do
  r <- runConduit (C.yieldMany [1 .. 10] .| await)
  print r -- => Just 1
```

`.|` で結合したトランスデューサのどれかが途中で止まった場合、他のトランスデューサもそこで停止します。
従ってこの例だと `yieldMany` も途中で止まっています。間に `mapM_` で `print` などを挟めば、途中で止まっているか検証できそうですが、今回は省略します。

さて、ConduitTはモナドなので、do文を使えばawaitを2回以上実行する事が可能です。

```haskell
main = do
  r <- runConduit (C.yieldMany [1 .. 10] .| sub)
  print r -- => 10203
  where
    sub = do
      Just x <- await
      Just y <- await
      Just z <- await
      return (x * 10000 + y * 100 + z)
```

同様に、出力もdo文で繋げられます。

## 実用編
実用という事で、ファイルの入出力をやってみます。

### ResourceTについて

基本的には今までと同じようにトランスデューサを走らせるのですが、入出力関係のトランスデューサは今までと少し違う型になっています。たとえばファイルから読み込むトランスデューサは以下の通り。

```haskell
sourceFile :: MonadResource m => FilePath -> ConduitT i ByteString m ()
```

`MonadResource` の要求がある場合、 `runConduit` の代わりに `runConduitRes` を使う必要があります。

```haskell
runConduitRes :: MonadUnliftIO m => ConduitT () Void (ResourceT m) r -> m r
```

ベースモナドが `ResourceT m` になっているので、`MonadResource` 条件を満たす事ができます。 `resourcet` パッケージで公開されている `runResourceT` を使って、以下のように書く事もできます。

```haskell
runResourceT (runConduit ...)
```

モナド変換子 `ResourceT` は、リソースの確保手段を提供します。 `runResourceT` を抜けると同時に、リソースの解放が行われます。これにより、ストリームが途中で止まったり例外を吐いたりしても、リソースリークを防止する事ができます。

ベースモナドが `IO` から `ResourceT IO` に変わった事で、今まで `putStrLn` で良かった所を `lift putStrLn` とか `liftIO putStrLn` とかにする必要があって面倒が増えますが、これは `unliftio` もしくは `rio` で公開されている `MonadIO` 対応版のIO関数を使えば省略できます。

### テキストの扱い
`sourceFile` の出力値は `ByteString` ですが、純粋に文字列として処理する場合は `Text` の方が便利なので、 `decodeUtf8` で `Text` のストリームにするといいと思います。

または、conduit-extraパッケージの[attoparsec連携](http://hackage.haskell.org/package/conduit-extra/docs/Data-Conduit-Attoparsec.html)を使ってパースしてしまうのも手です。

文字単位で処理する場合は、Textに変換したあと[concat](http://hackage.haskell.org/package/conduit/docs/Data-Conduit-Combinators.html#v:concat)トランスデューサを使うと `Char` のストリームに変換できますが[^3]、可能ならテキスト単位で受け取って純粋関数で処理したり、～～Eという名前で公開されている、要素処理用のトランスデューサを使った方が効率が良いようです。

あとは適当に、標準入出力とかファイル入出力とかを叩いてみれば、実用に十分なコンソールアプリが作れると思います。

簡単ですが以上です。よい年末を！

[^1]: 1.2系以前は演算子がもっといろいろあったので、実際すごく簡単になってます。
[^2]: 一番右以外の値が欲しい場合は `.|` の代わりに `fuseUpstream` などを使います。
[^3]: `ByteString` のまま同じ事をするとUTF8の生バイト列が見られるので、効率を追い求める人におすすめです。私はやりたくない。