Was bedeutet das Ausrufezeichen in einer Haskell-Erklärung?

261

Ich bin auf die folgende Definition gestoßen, als ich versuchte, Haskell mit einem echten Projekt zu lernen, um es voranzutreiben. Ich verstehe nicht, was das Ausrufezeichen vor jedem Argument bedeutet, und meine Bücher schienen es nicht zu erwähnen.

data MidiMessage = MidiMessage !Int !MidiMessage
David
quelle
13
Ich vermute, dass dies eine sehr häufige Frage sein könnte; Ich erinnere mich noch genau daran, wie ich mich vor langer Zeit über genau dasselbe gewundert habe.
cjs

Antworten:

313

Es ist eine Strengeerklärung. Grundsätzlich bedeutet dies, dass es bei der Erstellung des Datenstrukturwerts als "schwache Kopfnormalform" ausgewertet werden muss. Schauen wir uns ein Beispiel an, damit wir sehen können, was dies bedeutet:

data Foo = Foo Int Int !Int !(Maybe Int)

f = Foo (2+2) (3+3) (4+4) (Just (5+5))

Die fobige Funktion gibt bei Auswertung einen "Thunk" zurück, dh den Code, der ausgeführt werden muss, um seinen Wert herauszufinden. Zu diesem Zeitpunkt existiert noch nicht einmal ein Foo, nur der Code.

Aber irgendwann könnte jemand versuchen, hinein zu schauen, wahrscheinlich durch eine Musterübereinstimmung:

case f of
     Foo 0 _ _ _ -> "first arg is zero"
     _           -> "first arge is something else"

Dies wird genug Code ausführen, um das zu tun, was es braucht, und nicht mehr. Es wird also ein Foo mit vier Parametern erstellt (da Sie nicht hineinschauen können, ohne dass es vorhanden ist). Das erste, da wir es testen, müssen wir es bis dahin bewerten 4, wo wir feststellen, dass es nicht übereinstimmt.

Der zweite muss nicht ausgewertet werden, da wir ihn nicht testen. Anstatt 6an diesem Speicherort gespeichert zu werden, speichern wir den Code nur für eine mögliche spätere Auswertung (3+3). Das wird nur dann zu einer 6, wenn jemand darauf schaut.

Der dritte Parameter hat jedoch ein !vor sich, wird also streng ausgewertet: (4+4)wird ausgeführt und 8an diesem Speicherort gespeichert.

Der vierte Parameter wird ebenfalls streng ausgewertet. Aber hier wird es etwas schwierig: Wir bewerten nicht vollständig, sondern nur die schwache normale Kopfform. Dies bedeutet, dass wir herausfinden, ob es etwas ist Nothingoder nicht Just, und das speichern, aber wir gehen nicht weiter. Das bedeutet, dass wir nicht speichern, Just 10sondern tatsächlich Just (5+5), und den Thunk im Inneren unbewertet lassen. Dies ist wichtig zu wissen, obwohl ich denke, dass alle Implikationen davon eher über den Rahmen dieser Frage hinausgehen.

Sie können Funktionsargumente auf dieselbe Weise mit Anmerkungen versehen, wenn Sie die BangPatternsSpracherweiterung aktivieren :

f x !y = x*y

f (1+1) (2+2)wird den Thunk zurückgeben (1+1)*4.

cjs
quelle
16
Das ist sehr hilfreich. Ich frage mich jedoch, ob die Haskell-Terminologie das Verständnis für Menschen mit Begriffen wie "schwache normale Kopfform", "streng" usw. komplizierter macht. Wenn ich dich richtig verstehe, klingt es wie das! Operator bedeutet einfach, den ausgewerteten Wert eines Ausdrucks zu speichern, anstatt einen anonymen Block zu speichern, um ihn später auszuwerten. Ist das eine vernünftige Interpretation oder steckt etwas mehr dahinter?
David
71
@ David Die Frage ist, wie weit der Wert zu bewerten ist. Normalform mit schwachem Kopf bedeutet: Bewerten Sie sie, bis Sie den äußersten Konstruktor erreichen. Normalform bedeutet, den gesamten Wert auszuwerten, bis keine nicht bewerteten Komponenten mehr vorhanden sind. Da Haskell alle Arten von Bewertungstiefen zulässt, verfügt es über eine umfangreiche Terminologie, um dies zu beschreiben. Sie finden diese Unterscheidungen nicht in Sprachen, die nur die Call-by-Value-Semantik unterstützen.
Don Stewart
8
@ David: Ich habe hier eine ausführlichere Erklärung geschrieben: Haskell: Was ist die Normalform eines schwachen Kopfes? . Obwohl ich keine Knallmuster oder Strenge-Annotationen erwähne, sind sie gleichbedeutend mit der Verwendung seq.
Hammar
1
Nur um sicherzugehen, dass ich verstanden habe: Bezieht sich diese Faulheit nur auf unbekannte Argumente zur Kompilierungszeit? Wenn der Quellcode wirklich eine Anweisung (2 + 2) hat , würde er dann immer noch auf 4 optimiert, anstatt einen Code zum Hinzufügen der bekannten Zahlen zu haben?
Hi-Angel
1
Hi-Angel, das hängt wirklich davon ab, wie weit der Compiler optimieren möchte. Obwohl wir Pony verwenden, um zu sagen, dass wir bestimmte Dinge zu einer schwachen normalen Kopfform zwingen möchten, haben wir (und brauchen oder wollen) auch keine Möglichkeit zu sagen: "Bitte stellen Sie sicher, dass Sie diesen Thunk noch nicht bewerten." Schritt weiter. " Aus semantischer Sicht kann man bei einem Thunk "n = 2 + 2" nicht einmal sagen, ob ein bestimmter Verweis auf n jetzt ausgewertet wird oder zuvor ausgewertet wurde.
cjs
92

Eine einfache Möglichkeit, den Unterschied zwischen strengen und nicht strengen Konstruktorargumenten zu erkennen, besteht darin, wie sie sich verhalten, wenn sie nicht definiert sind. Gegeben

data Foo = Foo Int !Int

first (Foo x _) = x
second (Foo _ y) = y

Da das nicht strenge Argument von nicht bewertet wird second, verursacht die Übergabe undefinedkein Problem:

> second (Foo undefined 1)
1

Aber das strenge Argument kann nicht sein undefined, auch wenn wir den Wert nicht verwenden:

> first (Foo 1 undefined)
*** Exception: Prelude.undefined
Chris Conway
quelle
2
Dies ist besser als die Antwort von Curt Sampson, da es tatsächlich vom Benutzer beobachtbare Effekte des !Symbols erklärt, anstatt sich mit internen Implementierungsdetails zu befassen.
David Grayson
26

Ich glaube, es ist eine Strenge Annotation.

Haskell ist eine reine und faule funktionale Sprache, aber manchmal kann der Aufwand an Faulheit zu viel oder verschwenderisch sein. Um dies zu bewältigen, können Sie den Compiler bitten, die Argumente für eine Funktion vollständig auszuwerten, anstatt Thunks zu analysieren.

Weitere Informationen finden Sie auf dieser Seite: Leistung / Strenge .

Chris Vest
quelle
Hmm, das Beispiel auf der Seite, auf die Sie verwiesen haben, scheint über die Verwendung von zu sprechen! beim Aufrufen einer Funktion. Dies scheint jedoch anders zu sein, als es in eine Typdeklaration aufzunehmen. Was vermisse ich?
David
Das Erstellen einer Instanz eines Typs ist auch ein Ausdruck. Sie können sich die Typkonstruktoren als Funktionen vorstellen, die angesichts der angegebenen Argumente neue Instanzen des angegebenen Typs zurückgeben.
Chris Vest
4
In der Tat, für alle Absichten und Zwecke, Typkonstruktoren sind Funktionen; Sie können sie teilweise anwenden, an andere Funktionen übergeben (z. B. map Just [1,2,3]um [Nur 1, Nur 2, Nur 3] zu erhalten) und so weiter. Ich finde es hilfreich, über die Fähigkeit nachzudenken, Muster mit ihnen abzugleichen, sowie über eine völlig unabhängige Einrichtung.
cjs