Was ist so schlimm an Lazy I / O?

89

Ich habe allgemein gehört, dass Produktionscode die Verwendung von Lazy I / O vermeiden sollte. Meine Frage ist, warum? Ist es jemals in Ordnung, Lazy I / O außerhalb des Herumspielens zu verwenden? Und was macht die Alternativen (zB Enumeratoren) besser?

Dan Burton
quelle

Antworten:

81

Lazy IO hat das Problem, dass die Freigabe der von Ihnen erworbenen Ressourcen etwas unvorhersehbar ist, da dies davon abhängt, wie Ihr Programm die Daten verwendet - das "Anforderungsmuster". Sobald Ihr Programm den letzten Verweis auf die Ressource löscht, wird der GC diese Ressource schließlich ausführen und freigeben.

Lazy Streams sind ein sehr praktischer Programmierstil. Deshalb sind Shell Pipes so unterhaltsam und beliebt.

Wenn jedoch die Ressourcen eingeschränkt sind (wie in Hochleistungsszenarien oder Produktionsumgebungen, in denen eine Skalierung bis an die Grenzen der Maschine erwartet wird), kann es eine unzureichende Garantie sein, sich bei der Bereinigung auf den GC zu verlassen.

Manchmal müssen Sie Ressourcen eifrig freigeben, um die Skalierbarkeit zu verbessern.

Was sind also die Alternativen zu Lazy IO, die nicht bedeuten, die inkrementelle Verarbeitung aufzugeben (was wiederum zu viele Ressourcen verbrauchen würde)? Nun, wir haben eine foldlbasierte Verarbeitung, auch bekannt als Iteratees oder Enumerators, die Oleg Kiselyov Ende der 2000er Jahre eingeführt hat und seitdem durch eine Reihe von netzwerkbasierten Projekten populär gemacht wurde.

Anstatt Daten als Lazy Streams oder in einem großen Stapel zu verarbeiten, abstrahieren wir stattdessen über eine auf Chunks basierende strikte Verarbeitung mit garantierter Finalisierung der Ressource, sobald der letzte Chunk gelesen wurde. Das ist die Essenz der iteratee-basierten Programmierung und eine, die sehr nette Ressourcenbeschränkungen bietet.

Der Nachteil von iteratee-basierten E / A ist, dass es ein etwas umständliches Programmiermodell hat (ungefähr analog zur ereignisbasierten Programmierung im Vergleich zu einer netten threadbasierten Steuerung). Es ist definitiv eine fortgeschrittene Technik in jeder Programmiersprache. Und für die überwiegende Mehrheit der Programmierprobleme ist Lazy IO völlig zufriedenstellend. Wenn Sie jedoch viele Dateien öffnen oder über viele Sockets sprechen oder auf andere Weise viele gleichzeitige Ressourcen verwenden, ist ein iterierter (oder Enumerator-) Ansatz möglicherweise sinnvoll.

Don Stewart
quelle
22
Da ich gerade einem Link zu dieser alten Frage aus einer Diskussion über faule E / A gefolgt bin, dachte ich, ich würde eine Notiz hinzufügen, dass seitdem ein Großteil der Unbeholfenheit von Iteraten durch neue Streaming-Bibliotheken wie Pipes und Conduit ersetzt wurde .
Ørjan Johansen
40

Dons hat eine sehr gute Antwort geliefert, aber er hat ausgelassen, was (für mich) eines der überzeugendsten Merkmale von Iteraten ist: Sie erleichtern das Nachdenken über die Speicherverwaltung, da alte Daten explizit beibehalten werden müssen. Erwägen:

average :: [Float] -> Float
average xs = sum xs / length xs

Dies ist ein bekanntes Speicherleck, da die gesamte Liste xsgespeichert werden muss, um sowohl sumals auch zu berechnen length. Es ist möglich, einen effizienten Verbraucher zu machen, indem man eine Falte schafft:

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'

Es ist jedoch etwas unpraktisch, dies für jeden Stream-Prozessor tun zu müssen. Es gibt einige Verallgemeinerungen ( Conal Elliott - Beautiful Fold Zipping ), aber sie scheinen sich nicht durchgesetzt zu haben. Mit Iteraten können Sie jedoch eine ähnliche Ausdrucksebene erzielen.

aveIter = uncurry (/) <$> I.zip I.sum I.length

Dies ist nicht so effizient wie eine Falte, da die Liste immer noch mehrmals wiederholt wird. Sie wird jedoch in Blöcken gesammelt, damit alte Daten effizient durch Müll gesammelt werden können. Um diese Eigenschaft zu brechen, muss die gesamte Eingabe explizit beibehalten werden, z. B. bei stream2list:

badAveIter = (\xs -> sum xs / length xs) <$> I.stream2list

Der Status von Iteraten als Programmiermodell ist in Arbeit, jedoch viel besser als noch vor einem Jahr. Wir lernen , was combinators nützlich sind (zB zip, breakE,enumWith ) und die weniger so, mit dem Ergebnis , dass die eingebauten in iteratees und combinators bietet ständig mehr Expressivität.

Das heißt, Dons ist richtig, dass sie eine fortgeschrittene Technik sind; Ich würde sie sicherlich nicht für jedes E / A-Problem verwenden.

John L.
quelle
25

Ich verwende ständig Lazy I / O im Produktionscode. Es ist nur unter bestimmten Umständen ein Problem, wie Don erwähnt hat. Aber nur um ein paar Dateien zu lesen, funktioniert es einwandfrei.

August
quelle
Ich benutze auch faule E / A. Ich wende mich an Iterate, wenn ich mehr Kontrolle über das Ressourcenmanagement haben möchte.
John L
20

Update: Kürzlich hat Oleg Kiseljov im Haskell-Café gezeigt, dass unsafeInterleaveST(das zur Implementierung von Lazy IO innerhalb der ST-Monade verwendet wird) sehr unsicher ist - es bricht das Argumentationsgleich. Er zeigt, dass es erlaubt, so zu konstruieren bad_ctx :: ((Bool,Bool) -> Bool) -> Bool , dass

> bad_ctx (\(x,y) -> x == y)
True
> bad_ctx (\(x,y) -> y == x)
False

obwohl ==ist kommutativ.


Ein weiteres Problem mit Lazy IO: Der eigentliche IO-Vorgang kann verschoben werden, bis es zu spät ist, beispielsweise nachdem die Datei geschlossen wurde. Zitat aus dem Haskell Wiki - Probleme mit faulen E / A :

Ein häufiger Anfängerfehler besteht beispielsweise darin, eine Datei zu schließen, bevor Sie sie gelesen haben:

wrong = do
    fileData <- withFile "test.txt" ReadMode hGetContents
    putStr fileData

Das Problem ist, dass withFile das Handle schließt, bevor fileData erzwungen wird. Der richtige Weg ist, den gesamten Code an withFile zu übergeben:

right = withFile "test.txt" ReadMode $ \handle -> do
    fileData <- hGetContents handle
    putStr fileData

Hier werden die Daten verbraucht, bevor withFile beendet wird.

Dies ist oft unerwartet und ein leicht zu machender Fehler.


Siehe auch: Drei Beispiele für Probleme mit faulen I / O .

Petr Pudlák
quelle
Tatsächlich zu kombinieren hGetContentsund withFileist sinnlos, da erstere das Handle in einen "pseudo-geschlossenen" Zustand versetzt und das Schließen für Sie (träge) übernimmt, sodass der Code genau dem entspricht readFileoder sogar openFileohne hClose. Das ist im Grunde , was faul I / O ist . Wenn Sie nicht verwenden readFile, getContentsoder hGetContentsSie verwenden nicht faul I / O. Zum Beispiel line <- withFile "test.txt" ReadMode hGetLinefunktioniert gut.
Dag
1
@Dag: Obwohl hGetContentsdas Schließen der Datei für Sie erledigt wird, ist es auch zulässig, sie selbst "früh" zu schließen, und es wird sichergestellt, dass Ressourcen vorhersehbar freigegeben werden.
Ben Millwood
17

Ein weiteres Problem mit Lazy IO, das bisher nicht erwähnt wurde, ist das überraschende Verhalten. In einem normalen Haskell-Programm kann es manchmal schwierig sein, vorherzusagen, wann jeder Teil Ihres Programms bewertet wird, aber glücklicherweise spielt es aufgrund der Reinheit keine Rolle, es sei denn, Sie haben Leistungsprobleme. Wenn Lazy IO eingeführt wird, wirkt sich die Auswertungsreihenfolge Ihres Codes tatsächlich auf seine Bedeutung aus. Änderungen, die Sie gewohnt sind, als harmlos zu betrachten, können zu echten Problemen führen.

Als Beispiel hier eine Frage zu Code, der vernünftig aussieht, aber durch verzögerte E / A verwirrender wird: withFile vs. openFile

Diese Probleme sind nicht immer tödlich, aber es ist eine andere Sache, über die man nachdenken muss, und ausreichend starke Kopfschmerzen, die ich persönlich bei faulen E / A vermeide, es sei denn, es gibt ein echtes Problem, die gesamte Arbeit im Voraus zu erledigen.

Ben Millwood
quelle