Was passiert, wenn Sie ein Programm kompilieren, das keine Eingabe benötigt? (Haskell IO Reinheitsprobleme (wieder))

7

putStrLn gibt bei Aufruf mit beliebigen Argumenten immer einen Wert vom Typ IO () zurück. Ich stimme zu, dass das rein ist, ich kann damit umgehen. Aber ist es referenziell transparent? Ich denke schon, denn für irgendeine gegebene Eingabe könnten Sie den Funktionsaufruf durch ein IO () ersetzen, das die korrekte Zeichenkette auf stdout werfen würde.

Also ich bin cool mit putStrLn , aber getLine , wenn sie ohne Argumente aufgerufen wird, könnte eine beliebige Anzahl von Dingen zurückgeben, vorausgesetzt sie sind vom Typ IO String . Das ist weder rein noch referentiell transparent oder?

Alberne pedantische Frage und es wird wahrscheinlich nicht ändern, wie ich meinen Code schreibe, aber ich möchte das wirklich ein für allemal festnageln. (Ich verstehe, dass die IO-Monade die Dinge korrekt abbildet, das ist nicht mein Problem)

Das wirft eine andere Frage für mich auf. Ist der Compiler intelligent genug, um ein Programm zu erkennen, das keine Eingabe benötigt? Zum Beispiel sage ich kompilieren

%Vor%

Ist GHC intelligent genug, um dieses Programm auf IO () zu reduzieren, was dazu führt, dass [2,3,4,5,6,7,8,9,10,11] ausgedruckt wird? Oder funktioniert es immer noch und evaluiert / führt alles zur Laufzeit aus? Gleiches gilt für beliebige Programme, bei denen keine Eingabe erforderlich ist. Benutzt GHC die Tatsache, dass das gesamte Programm referenziell transparent ist und einfach durch seinen Wert ersetzt werden kann?

    
TheIronKnuckle 05.12.2011, 08:07
quelle

6 Antworten

7

Ich denke, es gibt zwei Fragen hier.

  1. Ist IO referentiell transparent
  2. Reduziert GHC willkürliche Ausdrücke zur Kompilierzeit

Wenn Sie sich den IO-Typ ansehen, können Sie sich vorstellen, dass er referenzielle Transparenz emuliert, indem er sich auf den mysteriösen Wert RealWorld stützt, der keinen Datenkonstruktor hat und jede Anweisung von der letzten abhängig macht (in einer einzigen Thread-Welt). . Im Fall von IO String ist dies ein Wrapper vom Typ newtype um RealWorld -> (RealWorld, String) ..., der eine Funktion und kein Wert ist. Die Verwendung von IO ohne die Monad Instanz macht dies besonders und schmerzhaft, offensichtlich.

%Vor%

Wie bei der GHC-Optimierung wird in diesem Fall die Liste beim Kompilieren nicht auf eine Zeichenkette reduziert. Der von GHC 7.2.1 erzeugte optimierte Code erzeugt eine Liste, mappt (+1) über den Ergebnissen, konvertiert die Liste in eine Zeichenkette und druckt sie schließlich auf der Konsole aus. So ziemlich genau wie es in deinem Beispiel steht.

    
Nathan Howell 05.12.2011, 08:47
quelle
7

Ja, diese monadischen Funktionen sind rein referenziell transparent, da die Substitutionsregel immer noch auf sie zutrifft.

In Haskell sind die folgenden beiden Programme gleichwertig

%Vor%

In einer "normalen" Sprache würde das zweite Beispiel nur einmal als Nebeneffekt der Auswertung von x drucken. Die Art und Weise, wie die beiden Programme tatsächlich gleich sind, wird ein wenig klarer, wenn Sie feststellen, dass ein Wert vom Typ IO() nicht wirklich eine Nebeneffekt-Berechnung ist, sondern eine Beschreibung einer solchen Berechnung ist kann als Baustein verwenden, um größere Berechnungen zu erstellen.

    
hugomg 05.12.2011 14:17
quelle
6

getLine :: IO String ist rein; Sein Wert ist die IO-Aktion, die * eine Zeichenfolge aus der Standardeingabe liest und zurückgibt. getLine ist immer gleich diesem Wert.

* Ich verwende hier das Wort "Returns" für das Fehlen eines besseren Wortes.

Wikipedia definiert referentielle Transparenz als:

  

Ein Ausdruck wird als referenziell transparent bezeichnet, wenn er durch seinen Wert ersetzt werden kann, ohne das Verhalten eines Programms zu ändern (mit anderen Worten, ein Programm zu liefern, das die gleichen Effekte hat und am selben Eingang ausgegeben wird).

Also ist getLine auch referentiell transparent. Obwohl ich mir keinen schönen Weg vorstellen kann, seinen "Wert" auf andere Weise auszudrücken, um "den Ausdruck durch seinen Wert zu ersetzen".

Man sollte auch vorsichtig mit Aussagen wie " putStrLn , wenn mit beliebigen Argumenten aufgerufen wird immer IO () zurückgeben". IO () ist ein Typ, kein Wert. Für jedes s :: String ist putStrLn s ein Wert vom Typ IO () , yes. Aber was dieser Wert ist, hängt natürlich von s ab.

(Außerdem, wenn Sie diese unsafe Dinge ausschließen, ist alles rein und referentiell transparent, und insbesondere auch getLine .)

    
Prateek 05.12.2011 09:14
quelle
4

Lassen Sie mich nur den zweiten Teil der Frage beantworten (ich habe den ersten Teil in einer früheren Frage beantwortet). Dem Compiler ist es freigestellt, mit dem Ausdruck zu tun, was er will, solange er die Semantik des Programms nicht ändert. Sie müssen also die Frage nach einem bestimmten Compiler stellen, damit er sinnvoll ist. Ist Ghc? Nein, nicht die aktuelle Version. Gibt es Compiler? Ja da ist.

    
augustss 05.12.2011 12:46
quelle
3

Ich bin nicht sicher, ob das helfen wird (ich entschuldige mich im Voraus, wenn es nur mehr verwirrt), aber die Art und Weise, wie IO in Mercury referenziell transparent gemacht wird, besteht darin, explizit einen Wert vom Typ io an alle IO-Performing zu übergeben Code, der auch einen neuen Wert vom Typ io zurückgeben muss.

Die Eingabe io repräsentiert "den Zustand der Welt" kurz bevor der Code aufgerufen wird. Die gesamte Welt außerhalb des Programms; Datenträgerinhalt, was auf dem Bildschirm gedruckt wird, was der Benutzer gerade schreibt, was vom Netz empfangen werden soll, alles.

Die Ausgabe io repräsentiert den Zustand der Welt kurz nachdem der Code aufgerufen wurde. Der Unterschied zwischen der Eingabe io und der Ausgabe io enthält die Änderungen an der Welt, die durch diesen Code gemacht wurden (plus allem anderen, was theoretisch extern passiert ist).

Das Mercury-Modus-System stellt sicher, dass Werte vom Typ io eindeutig sind; Es gibt immer nur einen von ihnen. Sie können also nicht denselben io -Wert an zwei verschiedene IO-Performing-Prozeduren übergeben. Sie übergeben ein io in eine Prozedur, machen es für Sie unbrauchbar und erhalten dann eine neue zurück.

Natürlich wird der tatsächliche Zustand der tatsächlichen Welt nicht in Werte vom Typ io codiert ; Tatsächlich ist io unter der Haube komplett leer! Es werden überhaupt keine Informationen weitergegeben! Aber die io Werte repräsentieren den Zustand der Welt.

Sie können sich Funktionen in der IO-Monade als das gleiche vorstellen. Sie nehmen ein zusätzliches implizites state-of-the-world-Argument und geben einen zusätzlichen impliziten state-of-the-world-Wert zurück. Die IO-Monade-Implementierung behandelt die Weiterleitung dieser zusätzlichen Ausgabe an die nächste Funktion. Dies macht die IO Monade sehr ähnlich der Staatsmonade; Es ist leicht zu sehen, dass get in dieser Monade rein ist, obwohl es scheinbar keine Argumente annimmt.

In diesem Verständnis erhält main den Ausgangszustand der Welt, bevor das Programm ausgeführt wird, und wandelt es nach Ausführung des Programms in den Zustand der Welt um, indem es den gesamten IO-Code in Ihrem Programm durchläuft.

Und weil Sie selbst keinen Status-of-the-World-Wert erhalten, haben Sie keine Möglichkeit, Ihre eigene kleine IO-Kette in der Mitte eines anderen Codes zu starten. Das ist es, was Reinheit garantiert, denn in Wirklichkeit können wir keine neue Welt mit einem eigenen Staat aus dem Nichts entstehen lassen.

So getLine :: IO String kann als etwas wie getLine :: World -> (World, String) angesehen werden. Es ist rein, weil all die verschiedenen Zeiten, die es aufgerufen wird und verschiedene Strings zurückgibt, jedes Mal ein anderes World erhalten hat.

Ganz gleich, ob Sie an Werte denken, die E / A-Aktionen sind, oder ob der Zustand der Welt zwischen Funktionen oder einem anderen Mechanismus weitergegeben wird, all diese Konstrukte sind repräsentativ . Unter der Haube wird alles IO mit unreinem Code implementiert, weil so die Welt funktioniert; Wenn Sie in eine Datei schreiben, haben Sie den Status der Festplatte geändert. Aber wir können dies auf einer höheren Abstraktionsebene darstellen, so dass Sie anders darüber nachdenken können.

Eine Analogie ist, dass Sie eine Karte mit Suchbäumen oder Hashtabellen oder auf eine andere Art und Weise implementieren können. Aber wenn Sie es implementiert haben, wenn Sie über Code denken, der die Map verwendet, denken Sie nicht über linke und rechte Subbäume oder Buckets und Hashes nach, Sie denken über die Abstraktion, die eine Map ist .

Wenn wir IO auf eine Weise darstellen können, die Reinheit und referenzielle Transparenz beibehält, können wir jede Argumentation anwenden, die referenzielle Transparenz erfordert, um Code zu verwenden, der diese Darstellung verwendet. Dies ermöglicht die Anwendung aller für einen solchen Code zutreffenden Mathematik (von denen viele bei der Implementierung fortgeschrittener Compiler für reinheitserzwungene Sprachen verwendet werden), selbst für Programme, die IO ausführen.

Und ein kurzer Nachtrag zu Ihrer zweiten Frage. GHC könnte dieses Eingabeprogramm theoretisch auf die Ausgabe reduzieren. Ich glaube aber nicht, dass es sehr schwierig ist, dies zu tun, denn das ist im Allgemeinen unentscheidbar. Stellen Sie sich ein Programm vor, das keine Eingabe gemacht, sondern eine unendliche Liste erstellt und dann die letzten drei Elemente gedruckt hat. Theoretisch kann jedes Programm, das nicht von seiner Eingabe abhängig ist, auf seine Ausgabe reduziert werden, aber um dies zu tun, muss der Compiler etwas tun, das der Ausführung des Programms zur Kompilierungszeit entspricht. Um dies ganz allgemein zu tun, müssen Sie froh sein, dass Ihre Programme manchmal zur Kompilierungszeit verwenden . Und fast jedes Programm ist abhängig von seiner Eingabe, so dass es nicht viel zu gewinnen gibt, wenn man das versucht.

Dort ist etwas zu erreichen, indem Teile von Programmen identifiziert werden, die von keiner Eingabe abhängig sind und sie durch ihr Ergebnis ersetzen. Dies wird als Teilbewertung bezeichnet und ist ein aktives Forschungsthema, aber es ist auch sehr schwierig und es gibt keine universelle Lösung.Um dies zu tun, müssen Sie in der Lage sein, Bereiche des Programms zu identifizieren, die den Compiler nicht in eine Endlosschleife schicken, um herauszufinden, was sie zurückgeben, und Sie müssen Entscheidungen treffen, ob Sie Code entfernen, der ein paar braucht Sekunden zur Laufzeit ist ein guter Vorteil, wenn es die Einbettung der Multi-hundert-Megabyte-Datenstruktur bedeutet, die es in der Binärdatei des Programms zurückgibt. Und Sie müssen diese ganze Analyse machen, ohne stundenlang zu brauchen, um mäßig komplexe Programme zu kompilieren.

    
Ben 06.12.2011 00:00
quelle
2

Zum zweiten Teil der Frage. Es gibt etwas, das Supercompilation genannt wird, das hoffentlich so etwas aufgreift. Es ist immer noch ein Forschungsgebiet.

    
TheIronKnuckle 05.12.2011 09:50
quelle