さまざまな遅延IO関連タスクにパイプ/コンジットライブラリを推奨している人を見てきました。これらのライブラリはどのような問題を正確に解決しますか?
また、ハッキング関連のライブラリを使用しようとすると、3つの異なるバージョンがある可能性が高いです。例:
これは私を混乱させます。私の解析タスクでは、attoparsecまたはpipes-attoparsec/attoparsec-conduitを使用する必要がありますか?単純なバニラattoparsecと比較して、パイプ/コンジットバージョンにはどのような利点がありますか?
レイジー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
プロデューサーに渡す必要があります。
これは本質的に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
の類似点に注意してください。コルーチンアクションの呼び出しを、これまでに作成したyield
のchunk
ingに置き換えるだけです。このyield
への呼び出しは、Nice消費パイプラインを構築するために、他のProducer
s型で構成できるIO
アクションの代わりにPipe
型を構築しますEffect IO ()
と呼ばれます。
このパイプ構築はすべて、実際にIO
アクションを呼び出さずに静的に行われます。これにより、pipes
を使用してコルーチンをより簡単に記述できます。 runEffect
main
アクションでIO
を呼び出すと、すべての効果が一度にトリガーされます。
_runEffect :: Effect IO () -> IO ()
_
では、なぜattoparsec
をpipes
にプラグインするのでしょうか。まあ、attoparsec
は遅延解析のために最適化されています。 attoparsec
パーサーにフィードされたチャンクを効果的な方法で作成している場合は、行き詰まりになります。あなたは出来る
pipes
(またはconduit
)を使用して、レイジーattoparsec
パーサーを含むコルーチンのシステムを構築し、必要なだけの入力を操作しながら、解析された値を生成します。ストリーム全体で可能な限り遅延させます。私の解析タスクでは、attoparsecまたはpipes-attoparsec/attoparsec-conduitを使用する必要がありますか?
両方とも pipes-attoparsec
およびattoparsec-conduit
指定されたattoparsec
Parser
をシンク/コンジットまたはパイプに変換します。したがって、どちらかの方法でattoparsec
を使用する必要があります。
単純なバニラattoparsecと比較して、パイプ/コンジットバージョンにはどのような利点がありますか?
それらは、バニラのパイプが機能しないパイプとコンジットで動作します(少なくともすぐに使用できるわけではありません)。
コンジットまたはパイプを使用せず、遅延IOの現在のパフォーマンスに満足している場合、特に大きなアプリケーションを作成していない場合や大きなファイルを処理していない場合は、現在のフローを変更する必要はありません。単にattoparsec
を使用できます。
ただし、レイジー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を使用する]を使用できます。
以下は、両方のライブラリの作者による素晴らしいポッドキャストです。
http://www.haskellcast.com/episode/006-gabriel-gonzalez-and-michael-snoyman-on-pipes-and-conduit/
ほとんどの質問に答えてくれます。
つまり、これらのライブラリはどちらもストリーミングの問題に取り組みます。ストリーミングの問題は、IOを扱うときに非常に重要です。本質的に、それらはチャンクでデータの転送を管理します。 RAM)を64KBだけ消費する1GBのファイルをサーバーとクライアントの両方で転送します。ストリーミングを行わないと、両端に同じ量のメモリを割り当てる必要がありました。
これらのライブラリの古い代替はレイジーIOですが、問題が多く、アプリケーションでエラーが発生しやすくなっています。これらの問題はポッドキャストで議論されています。
これらのライブラリのどれを使用するかについては、好みの問題です。 「パイプ」が好きです。詳細な違いはポッドキャストでも議論されています。