量産コードではレイジーI/Oの使用を避けるべきだと一般的に聞いています。私の質問は、なぜですか?いじるだけでなく、Lazy I/Oを使用しても問題ありませんか?そして、代替物(例えば列挙子)をより良くするものは何ですか?
Lazy IOは、プログラムがどのようにデータを消費するかに依存するため、取得したリソースの解放はいくらか予測できないという問題があります-その「要求パターン」。プログラムが最後の参照をドロップするとリソースに対して、GCは最終的に実行され、そのリソースを解放します。
レイジーストリームは、プログラムするのにとても便利なスタイルです。シェルパイプがとても楽しく人気があるのはこのためです。
ただし、リソースが制限されている場合(高性能シナリオ、またはマシンの限界までスケーリングすることが期待される実稼働環境など)、GCに依存してクリーンアップを行うことは、不十分な保証になる可能性があります。
スケーラビリティを向上させるために、リソースを積極的に解放する必要がある場合があります。
では、lazy IOの代わりに、インクリメンタル処理を放棄する(つまり、リソースを大量に消費する)ことを意味しません)に代わるものは何ですか?さて、foldl
ベースの処理があります。 、別名イテレートまたは列挙子、2000年代後半のOleg Kiselyovによって導入され、ネットワークベースのプロジェクト。
データをレイジーストリームとして、または1つの巨大なバッチで処理する代わりに、チャンクベースの厳密な処理を抽象化し、最後のチャンクが読み込まれるとリソースのファイナライズが保証されます。それがiterateeベースのプログラミングの本質であり、非常に素晴らしいリソース制約を提供するものです。
IterateeベースのIOの欠点は、やや厄介なプログラミングモデル(イベントベースのプログラミングとほぼ同じですが、Niceスレッドベースの制御に類似しています)です。これは、明らかに高度なテクニックです。任意のプログラミング言語です。そして、プログラミング問題の大部分については、lazy IOで十分です。ただし、多くのファイルを開いたり、多数のソケットで通信したり、その他の方法で同時に多くのリソースを使用したりする場合、iteratee(または列挙子)アプローチは意味があるかもしれません。
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
)を学習しています。その結果、組み込みイテレートとコンビネーターは継続的により多くの結果を提供します。表現力。
とは言っても、ドンはそれらが高度な技術であることは正しいです。私は確かにすべてのI/O問題にそれらを使用することはありません。
私は常に製品コードでレイジーI/Oを使用しています。それはドンが述べたように、特定の状況でのみ問題です。ただし、いくつかのファイルを読み取るだけの場合は問題なく動作します。
更新:最近haskell-cafe Oleg Kiseljovが示しました that unsafeInterleaveST
(lazy =の実装に使用されます= 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が完了する前にデータが消費されます。
これは多くの場合予期せぬエラーであり、簡単にエラーが発生します。
参照: レイジーI/Oの問題の3つの例 。
レイジーIOこれまでに言及されていないもう1つの問題は、驚くべき動作をすることです。通常のHaskellプログラムでは、プログラムの各部分がいつ評価されるかを予測するのが難しい場合がありますが、幸いにも純粋さのため、パフォーマンスの問題がない限り、問題にはなりません。遅延IOが導入されると、コードの評価順序は実際にその意味に影響を与えるため、無害だと考えることに慣れていると、本当の問題を引き起こす可能性があります。
例として、妥当なように見えても遅延IOによって混乱を招くコードに関する質問を次に示します。 withFile vs. openFile
これらの問題は必ずしも致命的ではありませんが、考えなければならないもう1つのことであり、すべての作業を前もって行うことに実際の問題がない限り、私が個人的に怠惰なIOを回避するのに十分深刻な頭痛です。