17
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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


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

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

実質的な選択肢は3つ。

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

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から探してきた yieldManymapM_ などのトランスデューサは、 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 の方が便利なので、 decodeUtf8Text のストリームにするといいと思います。

または、conduit-extraパッケージのattoparsec連携を使ってパースしてしまうのも手です。

文字単位で処理する場合は、Textに変換したあとconcatトランスデューサを使うと Char のストリームに変換できますが3、可能ならテキスト単位で受け取って純粋関数で処理したり、~~Eという名前で公開されている、要素処理用のトランスデューサを使った方が効率が良いようです。

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

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

  1. 1.2系以前は演算子がもっといろいろあったので、実際すごく簡単になってます。

  2. 一番右以外の値が欲しい場合は .| の代わりに fuseUpstream などを使います。

  3. ByteString のまま同じ事をするとUTF8の生バイト列が見られるので、効率を追い求める人におすすめです。私はやりたくない。

17
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?