web-dev-qa-db-ja.com

パイプ/コンジットが解決しようとしているものは何ですか

さまざまな遅延IO関連タスクにパイプ/コンジットライブラリを推奨している人を見てきました。これらのライブラリはどのような問題を正確に解決しますか?

また、ハッキング関連のライブラリを使用しようとすると、3つの異なるバージョンがある可能性が高いです。例:

これは私を混乱させます。私の解析タスクでは、attoparsecまたはpipes-attoparsec/attoparsec-conduitを使用する必要がありますか?単純なバニラattoparsecと比較して、パイプ/コンジットバージョンにはどのような利点がありますか?

49
Sibi

レイジーIO

レイジーIOはこのように機能します

_readFile :: FilePath -> IO ByteString
_

ここで、ByteStringはチャンク単位でのみ読み取られることが保証されています。そうするために、私たちは(ほとんど)書くことができました

_-- given `readChunk` which reads a chunk beginning at n
readChunk :: FilePath -> Int -> IO (Int, ByteString)

readFile fp = readChunks 0 where
  readChunks n = do
    (n', chunk) <- readChunk fp n
    chunks      <- readChunks n'
    return (chunk <> chunks)
_

しかし、ここでは、IOアクション_readChunks n'_がchunkとして利用可能な部分的な結果を返す前に実行されることに注意してください。これは、まったく怠惰ではないことを意味します。これに対抗するには、unsafeInterleaveIOを使用します

_readFile fp = readChunks 0 where
  readChunks n = do
    (n', chunk) <- readChunk fp n
    chunks      <- unsafeInterleaveIO (readChunks n')
    return (chunk <> chunks)
_

これにより、_readChunks n'_がすぐに返され、サンクが強制された場合にのみIO actionが実行されます。

これは危険な部分です。unsafeInterleaveIOを使用することで、IOのアクションを、ByteStringのチャンクをどのように消費するかに依存する非決定論的なポイントに延期しました。 。

コルーチンの問題を修正する

私たちがやりたいことは、readChunkの呼び出しとreadChunksの再帰の間にチャンク処理ステップをスライドさせることです。

_readFileCo :: Monoid a => FilePath -> (ByteString -> IO a) -> IO a
readFileCo fp action = readChunks 0 where
  readChunks n = do
    (n', chunk) <- readChunk fp n
    a           <- action chunk
    as          <- readChunks n'
    return (a <> as)
_

これで、各小さなチャンクが読み込まれた後に、任意のIOアクションを実行する機会を得ました。これにより、ByteStringをメモリに完全に読み込まなくても、より多くの作業を段階的に行うことができます。残念ながら、それはひどく構成的なものではありません。実行するには、消費量actionをビルドしてByteStringプロデューサーに渡す必要があります。

パイプベースのIO

これは本質的にpipesが解決するものです。これにより、効果的なコルーチンを簡単に作成できます。たとえば、ファイルリーダーをProducerとして記述します。これは、最終的にその効果が実行されたときにファイルのチャンクを「ストリーミング」していると考えることができます。

_produceFile :: FilePath -> Producer ByteString IO ()
produceFile fp = produce 0 where
  produce n = do
    (n', chunk) <- liftIO (readChunk fp n)
    yield chunk
    produce n'
_

このコードと上記のreadFileCoの類似点に注意してください。コルーチンアクションの呼び出しを、これまでに作成したyieldchunkingに置き換えるだけです。このyieldへの呼び出しは、Nice消費パイプラインを構築するために、他のProducers型で構成できるIOアクションの代わりにPipe型を構築しますEffect IO ()と呼ばれます。

このパイプ構築はすべて、実際にIOアクションを呼び出さずに静的に行われます。これにより、pipesを使用してコルーチンをより簡単に記述できます。 runEffectmainアクションでIOを呼び出すと、すべての効果が一度にトリガーされます。

_runEffect :: Effect IO () -> IO ()
_

Attoparsec

では、なぜattoparsecpipesにプラグインするのでしょうか。まあ、attoparsecは遅延解析のために最適化されています。 attoparsecパーサーにフィードされたチャンクを効果的な方法で作成している場合は、行き詰まりになります。あなたは出来る

  1. Strict IOを使用し、文字列全体をメモリにロードして、パーサーで遅延して消費するだけです。これは単純で予測可能ですが、非効率的です。
  2. レイジーIOを使用すると、本番環境が実際に実行される時期について推論できなくなりますIO効果が実際に実行され、消費スケジュールに従ってリソースリークまたはクローズドハンドル例外が発生する可能性があります。これは(1)よりも効率的ですが、簡単に予測できなくなる可能性があります。または、
  3. pipes(またはconduit)を使用して、レイジーattoparsecパーサーを含むコルーチンのシステムを構築し、必要なだけの入力を操作しながら、解析された値を生成します。ストリーム全体で可能な限り遅延させます。
61
J. Abrahamson

Attoparsecを使用する場合は、attoparsecを使用します

私の解析タスクでは、attoparsecまたはpipes-attoparsec/attoparsec-conduitを使用する必要がありますか?

両方とも pipes-attoparsecおよびattoparsec-conduit指定されたattoparsecParserをシンク/コンジットまたはパイプに変換します。したがって、どちらかの方法でattoparsecを使用する必要があります。

単純なバニラattoparsecと比較して、パイプ/コンジットバージョンにはどのような利点がありますか?

それらは、バニラのパイプが機能しないパイプとコンジットで動作します(少なくともすぐに使用できるわけではありません)。

コンジットまたはパイプを使用せず、遅延IOの現在のパフォーマンスに満足している場合、特に大きなアプリケーションを作成していない場合や大きなファイルを処理していない場合は、現在のフローを変更する必要はありません。単にattoparsecを使用できます。

ただし、レイジーIOの欠点を知っていることを前提としています。

レイジーIOの問題は何ですか? (問題調査withFile

最初の質問を忘れないでください:

これらのライブラリはどのような問題を正確に解決しますか?

これらは、遅延IOの関数型言語内で発生するストリーミングデータの問題( 1 およびを参照)を解決します。レイジーIOは、必要な結果が得られない場合があります(以下の例を参照)。特定のレイジー操作に必要な実際のシステムリソースを判断するのが難しい場合があります(データは、チャンクで読み書きされます/バイト/バッファリング/ onclose/onopen…)。

怠惰の例

import System.IO
main = withFile "myfile" ReadMode hGetContents
       >>= return . (take 5)
       >>= putStrLn

データの評価はputStrLnで行われるため、これは何も出力しませんが、ハンドルはこの時点ですでに閉じられています。

有毒酸で火を固定する

次のスニペットはこれを修正しますが、別の厄介な機能があります:

main = withFile "myfile" ReadMode $ \handle -> 
           hGetContents handle
       >>= return . (take 5)
       >>= putStrLn

この場合、hGetContentsすべてのファイルを読み取りますが、最初は予期していませんでした。サイズが数GBになる可能性のあるファイルのマジックバイトを確認したいだけの場合、これは適切な方法ではありません。

withFileを正しく使用する

解決策は、明らかに、takeコンテキスト内のwithFileのものです。

main = withFile "myfile" ReadMode $ \handle -> 
           fmap (take 5) (hGetContents handle)
       >>= putStrLn

ちなみに、これも解決策です パイプの作者が述べた

この[..]は、人々がpipesについて私に時々尋ねる質問に答えます。これをここで段階的に説明します。

リソース管理がpipesの中心的な焦点ではない場合、なぜ遅延IOではなくpipesを使用する必要があるのですか?

この質問をする多くの人々は、リソース管理の面で怠惰なIO問題を解決したOlegを介してストリームプログラミングを発見しました。しかし、私はこの議論が単独で説得力があるとは思いませんでした。ほとんどのリソース管理を解決できます。次のように、リソース取得をレイジーIOから分離するだけで問題が発生します:[上記の最後の例を参照]

これにより、前のステートメントに戻ります。

遅延IOの欠点を知っていると仮定すると、単にattoparsec [...] [遅延IOを使用する]を使用できます。

参考文献

18
Zeta

以下は、両方のライブラリの作者による素晴らしいポッドキャストです。

http://www.haskellcast.com/episode/006-gabriel-gonzalez-and-michael-snoyman-on-pipes-and-conduit/

ほとんどの質問に答えてくれます。


つまり、これらのライブラリはどちらもストリーミングの問題に取り組みます。ストリーミングの問題は、IOを扱うときに非常に重要です。本質的に、それらはチャンクでデータの転送を管理します。 RAM)を64KBだけ消費する1GBのファイルをサーバーとクライアントの両方で転送します。ストリーミングを行わないと、両端に同じ量のメモリを割り当てる必要がありました。

これらのライブラリの古い代替はレイジーIOですが、問題が多く、アプリケーションでエラーが発生しやすくなっています。これらの問題はポッドキャストで議論されています。

これらのライブラリのどれを使用するかについては、好みの問題です。 「パイプ」が好きです。詳細な違いはポッドキャストでも議論されています。

13
Nikita Volkov