量産コードではレイジーI / Oの使用を避けるべきだと一般的に聞いています。私の質問は、なぜですか?いじるだけでなく、Lazy I / Oを使用しても問題ありませんか?そして、何が代替案(例えば列挙子)をより良くするのですか?
回答:
レイジーIOには、プログラムがデータを消費する方法(その「要求パターン」)に依存するため、取得したリソースを解放することはいくぶん予測不可能であるという問題があります。プログラムがリソースへの最後の参照を削除すると、GCは最終的に実行され、そのリソースを解放します。
遅延ストリームはプログラムするのに非常に便利なスタイルです。これがシェルパイプがとても楽しく人気がある理由です。
ただし、リソースが制限されている場合(高性能シナリオ、またはマシンの限界までスケーリングすることが期待される実稼働環境など)、GCに依存してクリーンアップを行うことは、不十分な保証になる可能性があります。
スケーラビリティを向上させるために、リソースを積極的に解放する必要がある場合があります。
では、インクリメンタル処理を中止することを意味しない、レイジーIOの代替手段は何でしょうか(これは、リソースの消費量が多すぎます)?まあ、私たちはfoldl
ベースの処理、別名イテレートまたは列挙子を持っています 2000年代後半にOleg Kiselyov、それ以来多くのネットワーキングベースのプロジェクトによって一般化されたます。
データをレイジーストリームとして、または1つの巨大なバッチで処理する代わりに、チャンクベースの厳密な処理を抽象化し、最後のチャンクが読み込まれるとリソースのファイナライズを保証します。それがiterateeベースのプログラミングの本質であり、非常に優れたリソース制約を提供するものです。
iterateeベースのIOの欠点は、やや扱いにくいプログラミングモデル(イベントベースのプログラミングとほぼ同じですが、スレッドベースの制御に似ている)があることです。これは間違いなく、あらゆるプログラミング言語において高度な技術です。そして、プログラミング問題の大部分にとって、遅延IOは完全に満足のいくものです。ただし、多数のファイルを開く場合、多数のソケットで通信する場合、またはその他の方法で多数の同時リソースを使用する場合は、反復(または列挙)アプローチが有効です。
Donsは非常に良い答えを提供してくれましたが、彼は(私にとって)iterateesの最も説得力のある機能の1つであるものを省いています:古いデータは明示的に保持する必要があるため、スペース管理を推論しやすくなります。検討してください:
average :: [Float] -> Float
average xs = sum xs / length xs
とのxs
両方を計算するにはリスト全体をメモリに保持する必要があるため、これはよく知られたスペースリークです。折りたたみを作成することで、効率的なコンシューマーを作成できます。sum
length
average2 :: [Float] -> Float
average2 xs = uncurry (/) <$> foldl (\(sumT, n) x -> (sumT+x, n+1)) (0,0) xs
-- N.B. this will build up thunks as written, use a strict pair and foldl'
しかし、すべてのストリームプロセッサに対してこれを実行する必要があるのはやや不便です。いくつかの一般化(Conal Elliott-Beautiful Fold Zipping)がありますが、それらは追いついていないようです。ただし、iterateesを使用すると、同様のレベルの表現が得られます。
aveIter = uncurry (/) <$> I.zip I.sum I.length
リストは依然として複数回繰り返されるため、これはフォールドほど効率的ではありませんが、古いデータを効率的にガベージコレクションできるように、チャンクで収集されます。そのプロパティを壊すためには、stream2listのように、入力全体を明示的に保持する必要があります。
badAveIter = (\xs -> sum xs / length xs) <$> I.stream2list
プログラミングモデルとしての反復の状態は進行中の作業ですが、1年前よりもはるかに優れています。我々は(コンビネータが有用であるかを学んでいる例えばzip
、breakE
、enumWith
)とされ、内蔵iterateesとコンビネータが継続的に、より表現力を提供し、その結果、とても少ないです。
とは言っても、ドンはそれらが高度な技術であることは正しいです。私は確かにすべてのI / O問題にそれらを使用することはありません。
アップデート:最近HaskellのカフェでオレグKiseljov示したことをunsafeInterleaveST
(STモナド内怠惰なIOを実装するために使用されている)非常に安全ではない-それは等式推論を破ります。彼はそれを構築することを可能にすることを示しているbad_ctx :: ((Bool,Bool) -> Bool) -> Bool
ように
> bad_ctx (\(x,y) -> x == y)
True
> bad_ctx (\(x,y) -> y == x)
False
たとえ==
可換です。
遅延IOの別の問題:実際のIO操作は、ファイルが閉じられた後など、手遅れになるまで延期される可能性があります。Haskell Wikiからの引用-レイジーIOの問題:
たとえば、一般的な初心者の間違いは、ファイルの読み取りが完了する前にファイルを閉じることです。
wrong = do fileData <- withFile "test.txt" ReadMode hGetContents putStr fileData
問題は、fileDataが強制される前にwithFileがハンドルを閉じることです。正しい方法は、すべてのコードをwithFileに渡すことです。
right = withFile "test.txt" ReadMode $ \handle -> do fileData <- hGetContents handle putStr fileData
ここでは、withFileが完了する前にデータが消費されます。
これはしばしば予期せぬことであり、簡単に作成できるエラーです。
hGetContents
しwithFile
て無意味です。そのため、コードはとまったく同じreadFile
か、openFile
なしでも同じhClose
です。これが基本的に遅延I / O です。を使用しない場合readFile
、getContents
またはhGetContents
遅延I / Oを使用していない場合。たとえば、line <- withFile "test.txt" ReadMode hGetLine
正常に動作します。
hGetContents
ファイルのクローズを処理しますが、自分で「早期」にクローズすることも許可され、リソースが予測どおりに解放されるようにします。
これまで言及されていないレイジーIOのもう1つの問題は、驚くべき動作をすることです。通常のHaskellプログラムでは、プログラムの各部分がいつ評価されるかを予測するのが難しい場合がありますが、幸い、純粋さのため、パフォーマンスの問題がない限り、問題はありません。レイジーIOが導入されると、コードの評価順序が実際にその意味に影響を与えるため、慣れ親しんでいる変更が無害であると考えると、真の問題が発生する可能性があります。
例として、妥当なように見えても、遅延IOによって混乱を招くコードに関する質問を次に示します:withFileとopenFile
これらの問題は常に致命的ではありませんが、考えなければならないもう1つの問題であり、すべての作業を事前に実行することに実際の問題がない限り、怠惰なIOを個人的に回避するほどの深刻な頭痛です。