Wie wird referenzielle Transparenz durchgesetzt?

8

In FP-Sprachen liefert das wiederholte Aufrufen einer Funktion mit denselben Parametern immer wieder dasselbe Ergebnis (dh referenzielle Transparenz).

Aber eine Funktion wie diese (Pseudocode):

function f(a, b) {
    return a + b + currentDateTime.seconds;
}

wird nicht dasselbe Ergebnis für dieselben Parameter zurückgeben.

Wie werden diese Fälle in FP behandelt?

Wie wird referenzielle Transparenz durchgesetzt? Oder ist es nicht und es hängt von den Programmierern ab, sich zu verhalten?

JohnDoDo
quelle
5
Es hängt von der Sprache ab, einige erzwingen überhaupt keine referenzielle Transparenz, andere verwenden das Typsystem, um referenziell transparente Funktionen von E / A zu trennen, z. B. Monaden in Haskell oder Eindeutigkeitstypen in Clean
jk.
1
Ein guter Typ - System wird verhindert , dass Sie rufen currentDateTimeaus f, in Sprachen , die referentielle Transparenz (wie Haskell) erzwingen. Ich werde jemand anderem eine detailliertere Antwort geben lassen :) (Hinweis: currentDateTimeHat IO, und dies wird in seiner Art
Andres F.

Antworten:

20

aund bsind Numbers, während currentDateTime.secondsein IO<Number>. Diese Typen sind nicht kompatibel. Sie können sie nicht addieren. Daher ist Ihre Funktion nicht gut typisiert und wird einfach nicht kompiliert. Zumindest geschieht dies in reinen Sprachen mit einem statischen Typsystem wie Haskell. In unreinen Sprachen wie ML, Scala oder F # ist es Sache des Programmierers, die referenzielle Transparenz sicherzustellen, und natürlich gibt es in dynamisch typisierten Sprachen wie Clojure oder Scheme kein statisches Typsystem, um referenzielle Transparenz zu erzwingen.

Jörg W Mittag
quelle
Es ist also nicht möglich, dass das Compiler- / Typsystem in Scala wie in Haskell referenzielle Transparenz für mich gewährleistet?
Cib
8

Ich werde versuchen, Haskells Ansatz zu veranschaulichen (ich bin nicht sicher, ob meine Intuition zu 100% korrekt ist, da ich kein Haskell-Experte bin, Korrekturen sind willkommen).

Ihr Code kann wie folgt in Haskell geschrieben werden:

import System.CPUTime

f :: Integer -> Integer -> IO Integer
f a b = do
          t <- getCPUTime
          return (a + b + (div t 1000000000000))

Wo ist also referentielle Transparenz? fist eine Funktion, die bei zwei Ganzzahlen aund beine Aktion erstellt, wie Sie am Rückgabetyp erkennen können IO Integer. Diese Aktion ist angesichts der beiden Ganzzahlen immer dieselbe, sodass die Funktion, die ein Paar von Ganzzahlen E / A-Aktionen zuordnet, referenziell transparent ist.

Wenn diese Aktion ausgeführt wird, hängt der von ihr erzeugte ganzzahlige Wert von der aktuellen CPU-Zeit ab: Das Ausführen von Aktionen ist KEINE Funktionsanwendung.

Zusammenfassen: In Haskell können Sie reine Funktionen verwenden, um komplexe Aktionen (Sequenzieren, Verfassen von Aktionen usw.) auf referenziell transparente Weise zu erstellen und zu kombinieren. Beachten Sie erneut, dass die obige Funktion im obigen Beispiel fkeine Ganzzahl zurückgibt: Sie gibt eine Aktion zurück.

BEARBEITEN

Einige weitere Details zur JohnDoDo-Frage.

Was bedeutet es, dass "das Ausführen von Aktionen KEINE Funktionsanwendung ist"?

Bei gegebenen Mengen T1, T2, Tn, T ist eine Funktion f eine Abbildung (Beziehung), die jedem Tupel in T1 x T2 x ... x Tn einen Wert in T zuordnet. Die Funktionsanwendung erzeugt also einen Ausgabewert bei bestimmten Eingabewerten . Mit diesem Mechanismus können Sie Ausdrücke erstellen, die zu Werten ausgewertet werden, z. B. ist der Wert 10das Ergebnis der Auswertung des Ausdrucks 4 + 6. Beachten Sie, dass Sie beim Zuordnen von Werten zu Werten auf diese Weise keine Eingabe / Ausgabe durchführen.

In Haskell sind Aktionen Werte spezieller Typen, die durch Auswerten von Ausdrücken mit geeigneten reinen Funktionen, die mit Aktionen arbeiten, erstellt werden können. Auf diese Weise ist ein Haskell-Programm eine zusammengesetzte Aktion, die durch Auswerten der mainFunktion erhalten wird. Diese Hauptaktion hat Typ IO ().

Sobald diese zusammengesetzte Aktion definiert wurde, wird ein anderer Mechanismus (keine Funktionsanwendung) verwendet, um die Aktion aufzurufen / auszuführen (siehe z . B. hier ). Die gesamte Programmausführung ist das Ergebnis des Aufrufs der Hauptaktion, die wiederum Unteraktionen aufrufen kann. Dieser Aufrufmechanismus (dessen interne Details ich nicht kenne) sorgt dafür, dass alle erforderlichen E / A-Aufrufe ausgeführt werden und möglicherweise auf das Terminal, die Festplatte, das Netzwerk usw. zugegriffen wird.

Zurück zum Beispiel. Die fobige Funktion gibt keine Ganzzahl zurück und Sie können keine Funktion schreiben, die E / A ausführt und gleichzeitig eine Ganzzahl zurückgibt: Sie müssen eine der beiden auswählen.

Sie können die zurückgegebene Aktion f 2 3in eine komplexere Aktion einbetten . Wenn Sie beispielsweise die durch diese Aktion erzeugte Ganzzahl drucken möchten, können Sie Folgendes schreiben:

main :: IO ()
main = do
          x <- f 2 3
          putStrLn (show x)

Die doNotation gibt an, dass die von der Hauptfunktion zurückgegebene Aktion durch eine sequentielle Zusammensetzung von zwei kleineren Aktionen erhalten wird, und die x <-Notation gibt an, dass der in der ersten Aktion erzeugte Wert an die zweite Aktion übergeben werden muss.

In der zweiten Aktion

putStrLn (show x)

Der Name xist an die Ganzzahl gebunden, die durch Ausführen der Aktion erzeugt wird

f 2 3

Ein wichtiger Punkt ist, dass die Ganzzahl, die beim Aufrufen der ersten Aktion erzeugt wird, nur innerhalb von E / A-Aktionen leben kann: Sie kann von einer E / A-Aktion zur nächsten übergeben werden, kann jedoch nicht als einfacher ganzzahliger Wert extrahiert werden.

Vergleichen Sie die mainobige Funktion mit dieser:

main = do
      let y = 2 + 3
      putStrLn (show y)

In diesem Fall gibt es nur eine Maßnahme, nämlich putStrLn (show y), und yist mit dem Ergebnis der Anwendung der reinen Funktion gebunden +. Wir könnten diese Hauptaktion auch wie folgt definieren:

main = putStrLn "5"

Beachten Sie also die unterschiedliche Syntax

x <- f 2 3    -- Inject the value produced by an action into
              -- the following IO actions.
              -- The value may depend on when the action is
              -- actually executed. What happens when the action is
              -- executed is not known here: it may get user input,
              -- access the disk, the network, the system clock, etc.

let y = 2 + 3 -- Bind y to the result of applying the pure function `+`
              -- to the arguments 2 and 3.
              -- The value depends only on the arguments 2 and 3.

Zusammenfassung

  • In Haskell werden reine Funktionen verwendet, um die Aktionen zu erstellen, die ein Programm bilden.
  • Aktionen sind Werte eines bestimmten Typs.
  • Da Aktionen durch Anwenden reiner Funktionen erstellt werden, ist die Aktionskonstruktion referenziell transparent.
  • Nachdem eine Aktion erstellt wurde, kann sie über einen separaten Mechanismus aufgerufen werden.
Giorgio
quelle
2
Würde es Ihnen etwas ausmachen, den executing actions is NOT function applicationSatz ein wenig zu beschreiben ? In meinem Beispiel wollte ich eine ganze Zahl zurückgeben. Was passiert, wenn eine Ganzzahl zurückgegeben wird?
JohnDoDo
2
@ JohnDoDo in Haskell zumindest mit Faulheit (ich kann keine eifrigen referenziell transparenten Sprachen sprechen) nichts wird ausgeführt, bis es unbedingt sein muss. Dies bedeutet, dass Giorgio in dem Beispiel gezeigt hat, dass Sie diese Aktion erhalten, und dass Sie, abgesehen von unappetitlichen Dingen, niemals die Nummer aus einer E / A-Aktion herausholen können , sondern diese Aktion mit anderen E / A-Aktionen in Ihrem Programm kombinieren müssen, bis Sie am Ende sind mit Main welche; Überraschung Überraschung ist eine IO-Aktion. Haskell selbst führt die E / A-Aktion aus, während der gesamten Ausführung jedoch nur die Teile, die erforderlich sind, und nur dann, wenn sie vorhanden sind.
Jimmy Hoffa
@JohnDoDo Wenn Sie eine Ganzzahl zurückgeben möchten, fkönnen Sie keinen Typ haben IO Integer(das ist eine Aktion, keine Ganzzahl). Dann kann es jedoch nicht das "aktuelle Datum" aufrufen, das einen Typ hat IO Integer.
Andres F.
Außerdem kann die E / A-Ganzzahl, die Sie als Ausgabe erhalten, nicht wieder in eine reguläre Ganzzahl konvertiert und dann wieder in reinem Code verwendet werden. Im Wesentlichen bleibt das, was in der E / A-Monade vor sich geht, in der E / A-Monade. (Es gibt eine Ausnahme, Sie könnten unsafePerformIO verwenden, um einen Wert wieder herauszuholen, aber indem Sie dies tun, sagen Sie dem Compiler im Wesentlichen, dass "Es ist in Ordnung, dies ist wirklich referenziell transparent". Der Compiler wird Ihnen und dem glauben Wenn Sie die Funktion das nächste Mal verwenden, wird möglicherweise der Wert der zuvor berechneten Funktion abgerufen, anstatt die aktuelle Zeit zu verwenden.)
Michael Shaw
1
Ihr letztes Beispiel zeigt nur, dass Sie keine passende Instanz von Showverfügbar haben. Sie können jedoch problemlos eine hinzufügen. In diesem Fall wird der Code kompiliert und funktioniert einwandfrei. IOHandlungen in Bezug auf sind nichts Besonderes show.
4

Der übliche Ansatz besteht darin, dem Compiler zu ermöglichen, zu verfolgen, ob eine Funktion im gesamten Aufrufdiagramm rein ist oder nicht, und Code abzulehnen, der Funktionen als rein deklariert, die unreine Dinge tun (wobei "Aufrufen einer unreinen Funktion" auch eine unreine Sache ist).

Haskell tut dies, indem er alles in der Sprache selbst rein macht ; Alles Unreine läuft zur Laufzeit, nicht die Sprache selbst. Die Sprache erstellt lediglich E / A-Aktionen mit reinen Funktionen. Die Laufzeit findet dann die reine Funktion, die mainvom angegebenen MainModul aufgerufen wird, wertet sie aus und führt die resultierende (unreine) Aktion aus.

Andere Sprachen sind pragmatischer; Ein üblicher Ansatz besteht darin, eine Syntax zum Markieren von Funktionen als "rein" hinzuzufügen und unreine Aktionen (Variablenaktualisierungen, Aufrufen unreiner Funktionen, E / A-Konstrukte) in solchen Funktionen zu verbieten.

In Ihrem Beispiel currentDateTimehandelt es sich um eine unreine Funktion (oder etwas, das sich wie eine verhält). Daher ist das Aufrufen innerhalb eines reinen Blocks verboten und würde einen Compilerfehler verursachen. In Haskell würde Ihre Funktion ungefähr so ​​aussehen:

f :: Int -> Int -> IO Int
f a b = do
    ct <- getCurrentTime
    return (a + b + timeSeconds ct)

Wenn Sie dies in einer Nicht-E / A-Funktion wie folgt versucht haben:

f :: Int -> Int -> Int
f a b =
    let ct = getCurrentTime
    in a + b + timeSeconds ct

... dann würde der Compiler Ihnen sagen, dass Ihre Typen nicht auschecken - getCurrentTimeist vom Typ IO Time, nicht Time, aber timeSecondserwartet Time. Mit anderen Worten, Haskell nutzt sein Typsystem, um die Reinheit zu modellieren (und durchzusetzen).

tdammers
quelle