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 foldl
basierte 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.
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:
Dies ist ein bekanntes Speicherleck, da die gesamte Liste
xs
gespeichert werden muss, um sowohlsum
als auch zu berechnenlength
. Es ist möglich, einen effizienten Verbraucher zu machen, indem man eine Falte schafft: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.
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:
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.
quelle
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.
quelle
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 konstruierenbad_ctx :: ((Bool,Bool) -> Bool) -> Bool
, dassobwohl
==
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 :
Dies ist oft unerwartet und ein leicht zu machender Fehler.
Siehe auch: Drei Beispiele für Probleme mit faulen I / O .
quelle
hGetContents
undwithFile
ist 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 entsprichtreadFile
oder sogaropenFile
ohnehClose
. Das ist im Grunde , was faul I / O ist . Wenn Sie nicht verwendenreadFile
,getContents
oderhGetContents
Sie verwenden nicht faul I / O. Zum Beispielline <- withFile "test.txt" ReadMode hGetLine
funktioniert gut.hGetContents
das 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.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.
quelle