Was sind Haskells Strengepunkte?

90

Wir alle wissen (oder sollten wissen), dass Haskell standardmäßig faul ist. Nichts wird ausgewertet, bis es ausgewertet werden muss. Wann muss also etwas bewertet werden? Es gibt Punkte, an denen Haskell streng sein muss. Ich nenne diese "Strenge Punkte", obwohl dieser spezielle Begriff nicht so weit verbreitet ist, wie ich gedacht hatte. Nach mir:

Eine Reduktion (oder Bewertung) in Haskell erfolgt nur an Strengepunkten.

Die Frage ist also: Was genau sind Haskells Strengepunkte? Meine Intuition sagt , dass main, seq/ Knall - Muster, Pattern - Matching, und jede IOAktion durchgeführt über maindie primären Strikt Punkte, aber ich weiß wirklich nicht , warum ich das wissen.

(Wenn sie nicht als "Strenge Punkte" bezeichnet werden, wie heißen sie dann?)

Ich kann mir vorstellen, dass eine gute Antwort eine Diskussion über WHNF und so weiter beinhalten wird. Ich stelle mir auch vor, dass es den Lambda-Kalkül berühren könnte.


Bearbeiten: zusätzliche Gedanken zu dieser Frage.

Da ich über diese Frage nachgedacht habe, denke ich, dass es klarer wäre, der Definition eines Strengepunkts etwas hinzuzufügen. Strenge Punkte können unterschiedliche Kontexte und unterschiedliche Tiefen (oder Strenge) haben. Zurück zu meiner Definition, dass "Reduktion in Haskell nur an Strengepunkten auftritt", fügen wir dieser Definition diese Klausel hinzu: "Ein Strengepunkt wird nur ausgelöst, wenn sein umgebender Kontext bewertet oder reduziert wird."

Lassen Sie mich versuchen, Ihnen die Art von Antwort zu geben, die ich möchte. mainist ein strenger Punkt. Es wird speziell als primärer Strengepunkt seines Kontextes bezeichnet: das Programm. Wenn das Programm ( mainder Kontext) ausgewertet wird, wird der Strengepunkt von main aktiviert. Die Tiefe des Mains ist maximal: Es muss vollständig ausgewertet werden. Main besteht normalerweise aus E / A-Aktionen, die auch Strengepunkte sind und deren Kontext ist main.

Jetzt versuchen Sie: Diskutieren seqund Mustervergleich in diesen Begriffen. Erklären Sie die Nuancen der Funktionsanwendung: Wie streng ist sie? Wie ist es nicht Was ist mit deepseq? letund caseAussagen? unsafePerformIO? Debug.Trace? Top-Level-Definitionen? Strenge Datentypen? Knallmuster? Wie viele dieser Elemente können nur in Form von Seq oder Pattern Matching beschrieben werden?

Dan Burton
quelle
10
Ihre intuitive Liste ist wahrscheinlich nicht sehr orthogonal. Ich vermute, dass seqund Mustervergleich ausreichend sind, wobei der Rest in Bezug auf diese definiert wird. Ich denke, Pattern Matching gewährleistet IOzum Beispiel die Strenge der Handlungen.
CA McCann
Primitive, wie +bei den eingebauten numerischen Typen, erzwingen ebenfalls Strenge, und ich gehe davon aus, dass dies auch für reine FFI-Aufrufe gilt.
Hammar
4
Hier scheinen zwei Konzepte zu verwechseln. Pattern Matching und Seq- und Bang-Patterns sind Möglichkeiten, wie ein Ausdruck in seinen Unterausdrücken streng werden kann - das heißt, wenn der oberste Ausdruck ausgewertet wird, ist dies auch der Unterausdruck. Auf der anderen Seite ist die Hauptausführung von E / A-Aktionen der Beginn der Evaluierung . Dies sind verschiedene Dinge, und es ist eine Art Typfehler, sie in dieselbe Liste aufzunehmen.
Chris Smith
@ChrisSmith Ich versuche nicht, diese beiden unterschiedlichen Fälle zu verwechseln. Wenn überhaupt, bitte ich um weitere Klarstellung, wie sie interagieren. Strenge geschieht irgendwie, und beide Fälle sind wichtige, wenn auch unterschiedliche Teile der Strenge, die "passieren". (und @ monadic: ಠ_ಠ)
Dan Burton
Wenn Sie Raum wünschen / brauchen, um Aspekte dieser Frage zu diskutieren, ohne eine vollständige Antwort zu versuchen, lassen Sie mich vorschlagen, die Kommentare in meinem / r / haskell-Beitrag für diese Frage zu verwenden
Dan Burton

Antworten:

46

Ein guter Anfang ist das Verständnis dieses Papiers: Eine natürliche Semantik für Lazy Evalution (Launchbury). Hier erfahren Sie, wann Ausdrücke für eine kleine Sprache ausgewertet werden, die dem Kern von GHC ähnelt. Die verbleibende Frage ist dann, wie das vollständige Haskell dem Kern zugeordnet werden kann. Der größte Teil dieser Übersetzung wird vom Haskell-Bericht selbst bereitgestellt. In GHC nennen wir diesen Prozess "Desugaring", weil er syntaktischen Zucker entfernt.

Nun, das ist nicht die ganze Geschichte, denn GHC enthält eine ganze Reihe von Optimierungen zwischen Desugaring und Codegenerierung, und viele dieser Transformationen werden den Kern neu anordnen, sodass die Dinge zu unterschiedlichen Zeiten bewertet werden (insbesondere die Strenge-Analyse führt dazu, dass die Dinge bewertet werden vorhin). Um wirklich zu verstehen, wie Ihr Programm bewertet wird, müssen Sie sich den von GHC erstellten Core ansehen.

Vielleicht erscheint Ihnen diese Antwort etwas abstrakt (ich habe weder Knallmuster noch Seq erwähnt), aber Sie haben nach etwas Genauem gefragt , und dies ist ungefähr das Beste, was wir tun können.

Simon Marlow
quelle
18
Ich fand es immer amüsant, dass in dem, was GHC "Desugaring" nennt, der zu entfernende syntaktische Zucker die eigentliche Syntax der Haskell-Sprache selbst enthält ... was impliziert, dass GHC in Wahrheit ein optimierender Compiler für den GHC ist Kernsprache, die übrigens auch ein sehr ausgeklügeltes Frontend für die Übersetzung von Haskell in Core enthält. :]
CA McCann
Die Typensysteme sind jedoch nicht genau ... insbesondere, aber nicht nur im Hinblick auf die Übersetzung von Typklassen in explizite Wörterbücher, wie ich mich erinnere. Und all die neuesten TF / GADT-Sachen haben, wie ich es verstehe, diese Lücke noch größer gemacht.
sclv
GCC optimiert C auch nicht: gcc.gnu.org/onlinedocs/gccint/Passes.html#Passes
György Andrasek
20

Ich würde diese Frage wahrscheinlich wie folgt umformulieren : Unter welchen Umständen wird Haskell einen Ausdruck bewerten? (Vielleicht eine "zu schwache Kopf-Normalform" anheften.)

In erster Näherung können wir dies wie folgt spezifizieren:

  • Durch Ausführen von E / A-Aktionen werden alle Ausdrücke ausgewertet, die sie "benötigen". (Sie müssen also wissen, ob die E / A-Aktion ausgeführt wird, z. B. der Name ist main, oder sie wird von main aufgerufen, UND Sie müssen wissen, was die Aktion benötigt.)
  • Ein Ausdruck, der ausgewertet wird (hey, das ist eine rekursive Definition!), Wertet alle benötigten Ausdrücke aus.

Aus Ihrer intuitiven Liste fallen Haupt- und E / A-Aktionen in die erste Kategorie, und Sequenz- und Mustervergleich fallen in die zweite Kategorie. Ich denke jedoch, dass die erste Kategorie eher Ihrer Vorstellung von "Strenge" entspricht, da wir auf diese Weise bewirken, dass die Bewertung in Haskell zu beobachtbaren Effekten für Benutzer wird.

Es ist eine große Aufgabe, alle Details spezifisch anzugeben, da Haskell eine große Sprache ist. Es ist auch ziemlich subtil, weil Concurrent Haskell die Dinge spekulativ bewerten kann, obwohl wir am Ende das Ergebnis nicht verwenden: Dies ist eine dritte Generation von Dingen, die eine Bewertung verursachen. Die zweite Kategorie ist recht gut untersucht: Sie möchten die Strenge der beteiligten Funktionen betrachten. Auch die erste Kategorie kann als eine Art "Strenge" angesehen werden, obwohl dies ein wenig zwielichtig ist, weil evaluate xund seq x $ return ()tatsächlich verschiedene Dinge sind! Sie können es richtig behandeln, wenn Sie der E / A-Monade eine Art Semantik geben (die explizite Übergabe eines RealWorld#Tokens funktioniert in einfachen Fällen), aber ich weiß nicht, ob es einen Namen für diese Art der geschichteten Strenge-Analyse im Allgemeinen gibt.

Edward Z. Yang
quelle
17

C hat das Konzept von Sequenzpunkten , die für bestimmte Operationen garantieren, dass ein Operand vor dem anderen ausgewertet wird. Ich denke, das ist das am nächsten existierende Konzept, aber der im Wesentlichen äquivalente Begriff Strengepunkt (oder möglicherweise Kraftpunkt ) entspricht eher dem Haskell-Denken.

In der Praxis ist Haskell keine rein faule Sprache: Zum Beispiel ist der Mustervergleich normalerweise streng (Wenn Sie also einen Mustervergleich versuchen, muss die Bewertung mindestens so weit erfolgen, dass der Abgleich akzeptiert oder abgelehnt wird.

Programmierer können das seqGrundelement auch verwenden, um die Auswertung eines Ausdrucks zu erzwingen, unabhängig davon, ob das Ergebnis jemals verwendet wird.

$!ist definiert in Bezug auf seq.

- Faul gegen nicht streng .

Ihr Denken über !/ $!und seqist also im Wesentlichen richtig, aber der Mustervergleich unterliegt subtileren Regeln. Sie können ~natürlich immer verwenden , um einen faulen Mustervergleich zu erzwingen. Ein interessanter Punkt aus demselben Artikel:

Der Strenge-Analysator sucht auch nach Fällen, in denen der äußere Ausdruck immer Unterausdrücke benötigt, und wandelt diese in eifrige Auswertungen um. Dies ist möglich, da sich die Semantik (in Bezug auf "unten") nicht ändert.

Lassen Sie uns das Kaninchenloch hinuntergehen und in den Dokumenten nach Optimierungen suchen, die von GHC durchgeführt wurden:

Die Strenge-Analyse ist ein Prozess, mit dem GHC versucht, zur Kompilierungszeit zu bestimmen, welche Daten definitiv "immer benötigt" werden. GHC kann dann Code erstellen, um nur solche Daten zu berechnen, und nicht den normalen Prozess (höherer Overhead) zum Speichern und späteren Ausführen der Berechnung.

- GHC-Optimierungen: Strenge Analyse .

Mit anderen Worten, strenger Code kann als Optimierung überall generiert werden , da das Erstellen von Thunks unnötig teuer ist, wenn die Daten immer benötigt werden (und / oder nur einmal verwendet werden dürfen).

… Es kann keine Bewertung mehr für den Wert durchgeführt werden; es soll in normaler Form sein . Wenn wir uns in einem der Zwischenschritte befinden, sodass wir zumindest eine Bewertung für einen Wert durchgeführt haben, liegt dieser in einer schwachen Kopfnormalform (WHNF) vor. (Es gibt auch eine "Kopf-Normalform", die in Haskell jedoch nicht verwendet wird.) Wenn Sie etwas in WHNF vollständig auswerten, wird es auf etwas in normaler Form reduziert.

- Wikibooks Haskell: Faulheit

(Ein Begriff ist in Kopf Normalform , wenn es kein Beta-redex in Position Kopf 1 . Ein redex ein Kopf redex ist , wenn es nur von Lambda Abstraktoren von nicht-Redexe ubervorangestellt ist 2 .) Also , wenn Sie beginnen einen Thunk zu zwingen, Sie arbeiten in WHNF; Wenn keine Schläge mehr zu erzwingen sind, sind Sie in normaler Form. Ein weiterer interessanter Punkt:

… Wenn wir irgendwann z z für den Benutzer ausdrucken müssten, müssten wir es vollständig auswerten…

Was natürlich impliziert , dass zwar jede IOausgeführte Aktion aus main tut Kraft Bewertung, die unter Berücksichtigung sollte klar sein , dass Haskell Programme tun, in der Tat, Dinge zu tun. Alles, was die in definierte Reihenfolge durchlaufen mainmuss, muss in normaler Form vorliegen und unterliegt daher einer strengen Bewertung.

CA McCann hat es jedoch in den Kommentaren richtig verstanden: Das einzige, was besonders mainist, mainist, dass es als speziell definiert wird; Der Mustervergleich auf dem Konstruktor ist ausreichend, um die von der IOMonade auferlegte Sequenz sicherzustellen . Nur in dieser Hinsicht sind seqMustervergleiche von grundlegender Bedeutung.

Jon Purdy
quelle
4
Tatsächlich ist das Zitat "Wenn wir zum Beispiel irgendwann z an den Benutzer ausdrucken müssten, müssten wir es vollständig bewerten" nicht ganz richtig. Es ist so streng wie die ShowInstanz für den zu druckenden Wert.
Nominolo
10

Haskell ist AFAIK keine reine faule Sprache, sondern eine nicht strenge Sprache. Dies bedeutet, dass Begriffe nicht unbedingt zum letztmöglichen Zeitpunkt bewertet werden.

Eine gute Quelle für Haskells Modell der "Faulheit" finden Sie hier: http://en.wikibooks.org/wiki/Haskell/Laziness

Grundsätzlich ist es wichtig, den Unterschied zwischen einem Thunk und der schwachen Header-Normalform WHNF zu verstehen.

Mein Verständnis ist, dass Haskell Berechnungen im Vergleich zu imperativen Sprachen rückwärts durchzieht. Dies bedeutet, dass es in Abwesenheit von "seq" - und Knallmustern letztendlich eine Art Nebeneffekt ist, der die Bewertung eines Thunks erzwingt, was wiederum zu vorherigen Bewertungen führen kann (wahre Faulheit).

Da dies zu einem schrecklichen Platzleck führen würde, findet der Compiler dann heraus, wie und wann Thunks im Voraus ausgewertet werden müssen, um Platz zu sparen. Der Programmierer kann diesen Prozess dann unterstützen, indem er Anmerkungen zur Strenge (en.wikibooks.org/wiki/Haskell/Strictness, www.haskell.org/haskellwiki/Performance/Strictness) gibt, um die Speicherplatznutzung in Form verschachtelter Thunks weiter zu reduzieren.

Ich bin kein Experte für die operative Semantik von Haskell, daher lasse ich den Link einfach als Ressource.

Einige weitere Ressourcen:

http://www.haskell.org/haskellwiki/Performance/Laziness

http://www.haskell.org/haskellwiki/Haskell/Lazy_Evaluation

flüssig
quelle
6

Faul heißt nicht nichts zu tun. Immer wenn Ihr Programmmuster mit einem caseAusdruck übereinstimmt , wertet es etwas aus - gerade genug. Andernfalls kann nicht herausgefunden werden, welche RHS verwendet werden soll. Sie sehen keine Fallausdrücke in Ihrem Code? Keine Sorge, der Compiler übersetzt Ihren Code in eine abgespeckte Form von Haskell, deren Verwendung schwer zu vermeiden ist.

Für einen Anfänger ist eine Faustregel letfaul, caseist weniger faul.

T_S_
quelle
2
Beachten Sie, dass die caseAuswertung in GHC Core zwar immer erzwungen wird, dies jedoch in regulärem Haskell nicht der Fall ist. Versuchen Sie es zum Beispiel case undefined of _ -> 42.
Hammar
2
casein GHC Core wertet sein Argument gegenüber WHNF aus, während casein Haskell sein Argument so weit wie nötig auswertet , um den entsprechenden Zweig auszuwählen. In Hammars Beispiel ist das überhaupt nicht so, aber case 1:undefined of x:y:z -> 42es wird tiefer ausgewertet als WHNF.
Max
Und muss auch case something of (y,x) -> (x,y)gar nicht auswerten something. Dies gilt für alle Produkttypen.
Ingo
@Ingo - das ist falsch. somethingmüsste zu WHNF ausgewertet werden, um den Tupelkonstruktor zu erreichen.
John L
John - warum? Wir wissen, dass es ein Tupel sein muss. Wo ist also der Sinn der Bewertung? Es reicht aus, wenn x und y an Code gebunden sind, der das Tupel auswertet und den entsprechenden Slot extrahiert, falls sie selbst jemals benötigt werden.
Ingo
4

Dies ist keine vollständige Antwort, die auf Karma abzielt, sondern nur ein Teil des Puzzles. In dem Maße, in dem es um Semantik geht, sollten Sie berücksichtigen, dass es mehrere Bewertungsstrategien gibt, die dieselbe Semantik bieten . Ein gutes Beispiel hier - und das Projekt spricht auch dafür, wie wir normalerweise über die Haskell-Semantik denken - war das Eager Haskell-Projekt, das die Bewertungsstrategien unter Beibehaltung derselben Semantik radikal veränderte : http://csg.csail.mit.edu/ pubs / haskell.html

sclv
quelle
2

Der Glasgow Haskell-Compiler übersetzt Ihren Code in eine Lambda-Kalkül-ähnliche Sprache namens Core . In dieser Sprache wird etwas ausgewertet, wenn Sie das Muster durch eine caseAnweisung abgleichen. Wenn also eine Funktion aufgerufen wird, wird der äußerste Konstruktor und nur dieser (wenn keine erzwungenen Felder vorhanden sind) ausgewertet. Alles andere ist in einem Thunk eingemacht. (Thunks werden durch letBindungen eingeführt).

Natürlich ist dies nicht genau das, was in der realen Sprache passiert. Der Compiler konvertiert Haskell auf sehr raffinierte Weise in Core und macht so viele Dinge wie möglich faul und alles, was immer benötigt wird, faul. Darüber hinaus gibt es Werte und Tupel ohne Box, die immer streng sind.

Wenn Sie versuchen, eine Funktion von Hand zu bewerten, können Sie im Grunde denken:

  • Versuchen Sie, den äußersten Konstruktor der Rückgabe zu bewerten.
  • Wenn etwas anderes benötigt wird, um das Ergebnis zu erhalten (aber nur, wenn es wirklich benötigt wird), wird es ebenfalls bewertet. Die Reihenfolge spielt keine Rolle.
  • Im Falle von IO müssen Sie die Ergebnisse aller Anweisungen vom ersten bis zum letzten darin auswerten. Dies ist etwas komplizierter, da die E / A-Monade einige Tricks ausführt, um die Auswertung in einer bestimmten Reihenfolge zu erzwingen.
fuz
quelle