Welche Regeln gibt es für eine Funktion a -> (), die in Haskell ausgewertet wird?

12

Wie der Titel schon sagt: Welche Garantien gibt es für die Bewertung einer Haskell-Funktionsrückgabeeinheit? Man könnte meinen, dass in einem solchen Fall keine Auswertung erforderlich ist. Der Compiler könnte alle derartigen Aufrufe durch einen unmittelbaren ()Wert ersetzen, es sei denn, es liegen explizite Anforderungen an die Strenge vor. In diesem Fall muss der Code möglicherweise entscheiden, ob dies der Fall sein soll Rückkehr ()oder Boden.
Ich habe in GHCi damit experimentiert, und es scheint, dass das Gegenteil passiert, das heißt, eine solche Funktion scheint bewertet zu werden. Ein sehr primitives Beispiel wäre

f :: a -> ()
f _ = undefined

Die Auswertung f 1löst aufgrund des Vorhandenseins von einen Fehler aus undefined, sodass definitiv eine Auswertung erfolgt. Es ist jedoch nicht klar, wie tief die Bewertung geht; manchmal scheint es so tief zu gehen, wie es notwendig ist, alle Aufrufe von zurückkehrenden Funktionen auszuwerten (). Beispiel:

g :: [a] -> ()
g [] = ()
g (_:xs) = g xs

Dieser Code wird auf unbestimmte Zeit wiederholt, wenn er mit angezeigt wird g (let x = 1:x in x). Aber dann

f :: a -> ()
f _ = undefined
h :: a -> ()
h _ = ()

kann verwendet werden, um zu zeigen, dass die h (f 1)Rückgabe ()erfolgt. In diesem Fall werden also nicht alle Unterausdrücke mit Einheitswert ausgewertet. Was ist hier die allgemeine Regel?

ETA: Natürlich weiß ich etwas über Faulheit. Ich frage, was Compiler-Autoren daran hindert, diesen speziellen Fall noch fauler als gewöhnlich zu machen.

ETA2: Zusammenfassung der Beispiele: GHC scheint ()genau wie jeder andere Typ zu behandeln , dh als ob es eine Frage gäbe, welcher reguläre Wert, der den Typ bewohnt, von einer Funktion zurückgegeben werden sollte. Die Tatsache, dass es nur einen solchen Wert gibt, scheint von den Optimierungsalgorithmen nicht (ab) verwendet zu werden.

ETA3: Wenn ich Haskell sage, meine ich Haskell-wie im Bericht definiert, nicht Haskell-the-H-in-GHC. Scheint eine Annahme zu sein, die nicht so weit verbreitet ist, wie ich es mir vorgestellt habe (was "von 100% der Leser" war), oder ich hätte wahrscheinlich eine klarere Frage formulieren können. Trotzdem bedauere ich, den Titel der Frage geändert zu haben, da ursprünglich gefragt wurde, welche Garantien für eine solche Funktion bestehen.

ETA4: Es scheint, dass diese Frage ihren Lauf genommen hat, und ich halte sie für unbeantwortet. (Ich suchte nach einer Funktion zum Schließen von Fragen, fand aber nur "Beantworte deine eigene Frage" und da diese nicht beantwortet werden kann, ging ich diesen Weg nicht.) Niemand brachte etwas aus dem Bericht hervor, das es so oder so entscheiden würde , was ich versucht bin, als starke, aber nicht eindeutige Antwort „Keine Garantie für die Sprache als solche“ zu interpretieren. Wir wissen nur, dass die aktuelle GHC-Implementierung die Bewertung einer solchen Funktion nicht überspringen wird.

Beim Portieren einer OCaml-App nach Haskell ist das eigentliche Problem aufgetreten. Die ursprüngliche App hatte eine gegenseitig rekursive Struktur vieler Typen, und der Code deklarierte eine Reihe von Funktionen, die assert_structureN_is_correctin 1..6 oder 7 für N aufgerufen wurden, von denen jede Einheit zurückgab, wenn die Struktur tatsächlich korrekt war, und eine Ausnahme auslöste, wenn dies nicht der Fall war . Außerdem haben sich diese Funktionen gegenseitig aufgerufen, da sie die Korrektheitsbedingungen zerlegt haben. In Haskell wird dies besser mit der Either StringMonade gehandhabt , also habe ich es so transkribiert, aber die Frage als theoretisches Problem blieb bestehen. Vielen Dank für alle Eingaben und Antworten.

Kryptozoon
quelle
1
Das ist Faulheit bei der Arbeit. Sofern das Ergebnis einer Funktion nicht angefordert wird (z. B. durch Mustervergleich mit einem Konstruktor), wird der Funktionskörper nicht bewertet. Versuchen Sie, h1::()->() ; h1 () = ()und zu vergleichen, um den Unterschied zu beobachten h2::()->() ; h2 _ = (). Führen Sie beide h1 (f 1)und aus h2 (f 1)und sehen Sie, dass nur der erste verlangt (f 1).
Chi
1
"Faulheit scheint zu diktieren, dass es durch () ersetzt wird, ohne dass irgendeine Bewertung stattfindet." Was bedeutet das? f 1wird undefinedin allen Fällen durch "ersetzt" .
Oisdk
3
Eine Funktion ... -> ()kann 1) beenden und zurückgeben (), 2) mit einem Ausnahme- / Laufzeitfehler beenden und nichts zurückgeben oder 3) divergieren (unendliche Rekursion). GHC optimiert den Code nicht unter der Annahme, dass nur 1) passieren kann: Wenn dies angefordert f 1wird, überspringt es nicht seine Auswertung und kehrt zurück (). Die Haskell-Semantik besteht darin, sie zu bewerten und zu sehen, was unter 1,2,3 passiert.
Chi
2
()In dieser Frage gibt es wirklich nichts Besonderes (weder den Typ noch den Wert). Alle die gleichen Beobachtungen passieren , wenn Sie ersetzen () :: ()mit, sagen wir, 0 :: Intüberall. Dies sind alles nur langweilige alte Konsequenzen einer faulen Bewertung.
Daniel Wagner
2
Nein, "Vermeiden" usw. ist nicht die Haskell-Semantik. und es gibt zwei mögliche Werte vom ()Typ ()und undefined.
Will Ness

Antworten:

10

Sie scheinen von der Annahme zu kommen, dass der Typ ()nur einen möglichen Wert hat (), und erwarten daher, dass bei jedem Funktionsaufruf, der einen Wert vom Typ ()zurückgibt, automatisch angenommen wird, dass er tatsächlich den Wert erzeugt ().

So funktioniert Haskell nicht. Jeder Typ hat einen weiteren Wert in Haskell, nämlich keinen Wert, Fehler oder so genannten "Bottom", der von codiert wird undefined. Somit findet tatsächlich eine Bewertung statt:

main = print (f 1)

entspricht der Kernsprache

main = _Case (f 1) _Of x -> print x   -- pseudocode illustration

oder sogar (*)

main = _Case (f 1) _Of x -> putStr "()"

und Core _Caseist zu zwingen :

"Die Auswertung eines %case[Ausdrucks] erzwingt die Auswertung des zu testenden Ausdrucks (der" Prüfer "). Der Wert des Prüflings ist an die Variable gebunden, die dem %ofSchlüsselwort folgt , ...".

Der Wert wird zur schwachen Kopfnormalform gezwungen. Dies ist Teil der Sprachdefinition.

Haskell ist nicht eine deklarative Programmiersprache.


(*) print x = putStr (show x) und show () = "()", so kann der showAufruf komplett kompiliert werden.

Der Wert ist in der Tat im Voraus bekannt als (), und sogar der Wert von show ()ist im Voraus bekannt als "()". Die akzeptierte Haskell-Semantik verlangt jedoch, dass der Wert von (f 1)zu einer schwachen Kopfnormalform gezwungen wird, bevor mit dem Drucken der im Voraus bekannten Zeichenfolge fortgefahren wird "()".


bearbeiten: Überlegen concat (repeat []). Sollte es sein []oder sollte es eine Endlosschleife sein?

Die Antwort einer "deklarativen Sprache" darauf ist höchstwahrscheinlich []. Haskells Antwort ist die Endlosschleife .

Faulheit ist "deklarative Programmierung des armen Mannes", aber es ist immer noch nicht die wirkliche Sache .

edit2 : print $ h (f 1) == _Case (h (f 1)) _Of () -> print ()und wird nur hgezwungen, nicht f; und um seine Antwort zu produzieren , hmuss gemäß seiner Definition nichts erzwungen werden h _ = ().

Abschiedsbemerkungen: Faulheit mag eine Existenzberechtigung haben, aber es ist nicht ihre Definition. Faulheit ist was es ist. Es ist definiert als alle Werte, die anfänglich Thunks sind und gemäß den Anforderungen, die von kommen, zu WHNF gezwungen werden main. Wenn es hilft, in einem bestimmten Fall je nach den spezifischen Umständen einen Boden zu vermeiden, dann tut es dies. Wenn nicht, nicht. Das ist alles.

Es hilft, es selbst in Ihrer Lieblingssprache zu implementieren, um ein Gefühl dafür zu bekommen. Wir können aber auch die Bewertung eines Ausdrucks verfolgen, indem wir alle Zwischenwerte sorgfältig benennen, sobald sie entstehen.


Nach dem Bericht haben wir

f :: a -> ()
f = \_ -> (undefined :: ())

dann

print (f 1)
 = print ((\ _ ->  undefined :: ()) 1)
 = print          (undefined :: ())
 = putStrLn (show (undefined :: ()))

und mit

instance Show () where
    show :: () -> String
    show x = case x of () -> "()"

es geht weiter

 = putStrLn (case (undefined :: ()) of () -> "()")

In Abschnitt 3.17.3 Formale Semantik der Musterübereinstimmung des Berichts heißt es nun

Die Semantik der caseAusdrücke ist in den Abbildungen 3.1–3.3 dargestellt. Jede Implementierung sollte sich so verhalten, dass diese Identitäten [...] gelten.

und Fall (r)in Abbildung 3.2 Zustände

(r)     case  of { K x1  xn -> e; _ -> e } =  
        where K is a data constructor of arity n 

() ist ein Datenkonstruktor der Arität 0, also ist es dasselbe wie

(r)     case  of { () -> e; _ -> e } =  

und das Gesamtergebnis der Bewertung ist somit .

Will Ness
quelle
2
Ich mag deine Erklärung. Es ist klar und einfach.
Menge
@ DanielWagner Ich hatte caseeigentlich das von Core im Sinn und ignorierte das klaffende Loch. :) Ich habe bearbeitet, um den Kern zu erwähnen.
Will Ness
1
Würde das Forcen nicht von showaufgerufen werden print? So etwas wieshow x = case x of () -> "()"
user253751
1
Ich beziehe mich auf caseCore, nicht auf Haskell. Haskell wird in Core übersetzt, der einen Zwang hat case, AFAIK. Sie haben Recht, dass casein Haskell nicht von selbst erzwingen. Ich könnte etwas in Schema oder ML (wenn ich ML schreiben könnte) oder Pseudocode schreiben.
Will Ness
1
Die maßgebliche Antwort auf all dies befindet sich wahrscheinlich irgendwo im Bericht. Ich weiß nur, dass hier keine "Optimierung" stattfindet und "regulärer Wert" in diesem Zusammenhang kein aussagekräftiger Begriff ist. Was auch immer gezwungen wird, gezwungen. printerzwingt so viel wie nötig zum Drucken. Der Typ wird nicht angezeigt. Die Typen sind verschwunden und werden gelöscht. Zum Zeitpunkt der Programmausführung ist die richtige Druckunterroutine bereits zur Kompilierungszeit ausgewählt und entsprechend dem Typ kompiliert. Diese Unterroutine wird ihren Eingabewert zur Laufzeit weiterhin auf WHNF erzwingen. Wenn sie nicht definiert wurde, verursacht sie einen Fehler.
Will Ness