Wie oft wird seq im Haskell-Produktionscode verwendet?

23

Ich habe einige Erfahrung mit dem Schreiben kleiner Werkzeuge in Haskell und finde es sehr intuitiv zu bedienen, insbesondere zum Schreiben von Filtern (unter Verwendung von Filtern interact), die ihre Standardeingabe verarbeiten und an die Standardausgabe weiterleiten.

Kürzlich habe ich versucht, einen solchen Filter für eine Datei zu verwenden, die etwa 10-mal so groß war wie gewöhnlich, und ich habe eine Stack space overflowFehlermeldung erhalten.

Nach einigem Lesen (z. B. hier und hier ) habe ich zwei Richtlinien identifiziert, um Stapelspeicher zu sparen (erfahrene Haskeller, bitte korrigieren Sie mich, wenn ich etwas schreibe, das nicht korrekt ist):

  1. Vermeiden Sie rekursive Funktionsaufrufe, die nicht tail-rekursiv sind (dies gilt für alle funktionalen Sprachen, die die Tail-Call-Optimierung unterstützen).
  2. Einführung seqin die Erzwingung einer frühen Auswertung von Unterausdrücken, damit die Ausdrücke nicht zu groß werden, bevor sie reduziert werden (dies gilt speziell für Haskell oder zumindest für Sprachen, die eine verzögerte Auswertung verwenden).

Nach fünf oder sechs seqAufrufen in meinem Code läuft mein Tool wieder reibungslos (auch auf den größeren Daten). Allerdings finde ich den Originalcode etwas besser lesbar.

Da ich kein erfahrener Haskell-Programmierer bin, wollte ich fragen, ob das Einführen seqauf diese Weise eine gängige Praxis ist und wie oft es normalerweise seqim Haskell-Produktionscode vorkommt. Oder gibt es Techniken, die es ermöglichen, zu häufiges Verwenden seqzu vermeiden und dennoch wenig Stapelspeicher zu verwenden?

Giorgio
quelle
1
Optimierungen wie die von Ihnen beschriebene werden den Code fast immer etwas weniger elegant machen.
Robert Harvey
@Robert Harvey: Gibt es alternative Techniken, um die Stapelverwendung gering zu halten? Ich meine, ich stelle mir vor, dass ich meine Funktionen anders schreiben muss, aber ich habe keine Ahnung, ob es gut etablierte Techniken gibt. Mein erster Versuch war die Verwendung rekursiver Funktionen, die mir geholfen haben, aber es mir nicht ermöglichten, mein Problem vollständig zu lösen.
Giorgio

Antworten:

17

Leider gibt es Fälle seq, in denen man ein effizientes / gut funktionierendes Programm für große Datenmengen verwenden muss. In vielen Fällen können Sie daher im Produktionscode nicht darauf verzichten. Weitere Informationen finden Sie in Real World Haskell, Kapitel 25. Profilerstellung und Optimierung .

Es gibt jedoch Möglichkeiten, eine seqdirekte Verwendung zu vermeiden . Dies kann den Code sauberer und robuster machen. Einige Ideen:

  1. Verwenden Sie stattdessen Conduit , Pipes oder Iterateesinteract . Lazy IO hat bekanntermaßen Probleme mit der Verwaltung von Ressourcen (nicht nur Speicher), und Iteratees sind genau darauf ausgelegt, diese zu überwinden. (Ich würde empfehlen, Lazy IO zu vermeiden, egal wie groß Ihre Daten sind - siehe Das Problem mit Lazy I / O. )
  2. Anstatt seqdirekt Kombinatoren wie foldl ' oder foldr' oder strenge Versionen von Bibliotheken (wie Data.Map.Strict oder Control.Monad.State.Strict ) zu verwenden (oder eigene zu entwerfen ), die für strenge Berechnungen ausgelegt sind.
  3. Benutze die BangPatterns Erweiterung. Es ermöglicht das Ersetzen seqdurch strengen Mustervergleich. In einigen Fällen kann es auch nützlich sein, strenge Konstruktorfelder zu deklarieren .
  4. Es ist auch möglich, Strategien zum Erzwingen der Bewertung zu verwenden. Die Strategies Library zielt hauptsächlich auf parallele Berechnungen ab, verfügt jedoch auch über Methoden zum Erzwingen eines Werts in WHNF ( rseq) oder Full NF ( rdeepseq). Es gibt viele nützliche Methoden zum Arbeiten mit Sammlungen, Kombinieren von Strategien usw.
Petr Pudlák
quelle
+1: Danke für die nützlichen Hinweise und Links. Punkt 3 scheint ziemlich interessant zu sein (und die für mich derzeit einfachste Lösung). In Bezug auf Vorschlag 1 sehe ich nicht, wie das Vermeiden von verzögertem E / A die Dinge verbessern kann: Nach meinem Verständnis sollte verzögertes E / A besser für einen Filter sein, der einen (möglicherweise sehr langen) Datenstrom verarbeiten soll.
Giorgio
2
@Giorgio Ich habe einen Link zu Haskell Wiki über Probleme mit Lazy IO hinzugefügt. Mit Lazy IO können Sie Ressourcen nur sehr schwer verwalten. Wenn Sie beispielsweise die Eingabe nicht vollständig lesen (z. B. aufgrund einer verzögerten Auswertung), bleibt das Datei-Handle geöffnet . Wenn Sie das Datei-Handle manuell schließen, wird es häufig aufgrund von verzögertem Auswertungslesen verschoben, und Sie schließen das Handle, bevor Sie die gesamte Eingabe lesen. Und es ist oft schwierig, Speicherprobleme mit Lazy IO zu vermeiden.
Petr Pudlák
Ich hatte vor kurzem dieses Problem und mein Programm lief aus Dateideskriptoren heraus. Also habe ich faul IO durch strenge IO ersetzt, indem ich strenge verwendet habe ByteString.
Giorgio,