メリークリスマス。
アドベントカレンダーの枠が埋まっていないので、ちょっとお邪魔してconduitをやります。
conduitはバージョン1.3系統で大きくAPIが変わっているのですが、日本語による新しい解説が見当たらないので、簡単にチュートリアルをやっていこうと思います。
Conduitは「読み込む→何かする→書き込む→繰り返し」みたいな処理をHaskellで書くためのライブラリ、またはフレームワークです。
本来、純粋なシーケンス相手であれば、こういう処理は得意なはずのHaskellなのですが、読み書きが入って来ると途端に話が複雑になります。
実質的な選択肢は3つ。
- 普通にdo文で手続き型っぽく書く
- Lazy IO
- なんかフレームワークを使う
1は実は最善策という説がありますが、やめておきましょう。2はトイプログラムには良いのですが、実用的にはいろいろ問題がある事が分かっています。
というわけで、3の選択肢、conduitを使っていきます。
最速入門
conduit
パッケージをお手元の環境に追加し、インポート文をぱぱっと書きます。
import Data.Conduit
import qualified Data.Conduit.Combinators as C
Data.Conduit.Combinatorsモジュールからトランスデューサ(ストリームを構成する部品)を探してきて、 .|
でつないで runConduit
します。
main = runConduit (C.yieldMany [1..5] .| C.mapM_ putStrLn)
1
2
3
4
5
ね、簡単でしょ。1
もう少し詳しく
ConduitTの型
Combinatorsから探してきた yieldMany
や mapM_
などのトランスデューサは、 ConduitT
という型を持ちます。
ConduitT i o m r
- i : 入力値
- o : 出力値
- m : ベースモナド
- r : 戻り値
出力値と戻り値があるんですね。
どう違うんでしょうか。
- 出力値 :
.|
でつないだとき後続のトランスデューサの入力になる - 戻り値 : runConduitの戻り値になる
なるほど。
なので、 sum
などのトランスデューサの結果を出すには runConduit
の戻り値を見る必要があります。
main = do
r <- runConduit (C.yieldMany [1 .. 10] .| C.sum)
print r -- => 55
なお .|
でつないだとき、全体の戻り値は一番右のトランスデューサのものになります。2
処理の終了と継続
今まで挙げたトランスデューサは、入力をすべて消費するまでループします。
一方、途中で return
して止まるやつもあります。
await
は、入力を1個だけ読んで止まります。
main = do
r <- runConduit (C.yieldMany [1 .. 10] .| await)
print r -- => Just 1
.|
で結合したトランスデューサのどれかが途中で止まった場合、他のトランスデューサもそこで停止します。
従ってこの例だと yieldMany
も途中で止まっています。間に mapM_
で print
などを挟めば、途中で止まっているか検証できそうですが、今回は省略します。
さて、ConduitTはモナドなので、do文を使えばawaitを2回以上実行する事が可能です。
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について
基本的には今までと同じようにトランスデューサを走らせるのですが、入出力関係のトランスデューサは今までと少し違う型になっています。たとえばファイルから読み込むトランスデューサは以下の通り。
sourceFile :: MonadResource m => FilePath -> ConduitT i ByteString m ()
MonadResource
の要求がある場合、 runConduit
の代わりに runConduitRes
を使う必要があります。
runConduitRes :: MonadUnliftIO m => ConduitT () Void (ResourceT m) r -> m r
ベースモナドが ResourceT m
になっているので、MonadResource
条件を満たす事ができます。 resourcet
パッケージで公開されている runResourceT
を使って、以下のように書く事もできます。
runResourceT (runConduit ...)
モナド変換子 ResourceT
は、リソースの確保手段を提供します。 runResourceT
を抜けると同時に、リソースの解放が行われます。これにより、ストリームが途中で止まったり例外を吐いたりしても、リソースリークを防止する事ができます。
ベースモナドが IO
から ResourceT IO
に変わった事で、今まで putStrLn
で良かった所を lift putStrLn
とか liftIO putStrLn
とかにする必要があって面倒が増えますが、これは unliftio
もしくは rio
で公開されている MonadIO
対応版のIO関数を使えば省略できます。
テキストの扱い
sourceFile
の出力値は ByteString
ですが、純粋に文字列として処理する場合は Text
の方が便利なので、 decodeUtf8
で Text
のストリームにするといいと思います。
または、conduit-extraパッケージのattoparsec連携を使ってパースしてしまうのも手です。
文字単位で処理する場合は、Textに変換したあとconcatトランスデューサを使うと Char
のストリームに変換できますが3、可能ならテキスト単位で受け取って純粋関数で処理したり、~~Eという名前で公開されている、要素処理用のトランスデューサを使った方が効率が良いようです。
あとは適当に、標準入出力とかファイル入出力とかを叩いてみれば、実用に十分なコンソールアプリが作れると思います。
簡単ですが以上です。よい年末を!