Ist das eine reine Funktion?

117

Die meisten Quellen definieren eine reine Funktion mit den folgenden zwei Eigenschaften:

  1. Der Rückgabewert ist für dieselben Argumente gleich.
  2. Die Bewertung hat keine Nebenwirkungen.

Es ist die erste Bedingung, die mich betrifft. In den meisten Fällen ist es leicht zu beurteilen. Berücksichtigen Sie die folgenden JavaScript-Funktionen (wie in diesem Artikel gezeigt ).

Rein:

const add = (x, y) => x + y;

add(2, 4); // 6

Unrein:

let x = 2;

const add = (y) => {
  return x += y;
};

add(4); // x === 6 (the first time)
add(4); // x === 10 (the second time)

Es ist leicht zu erkennen, dass die 2. Funktion unterschiedliche Ausgaben für nachfolgende Aufrufe liefert, wodurch die erste Bedingung verletzt wird. Und daher ist es unrein.

Diesen Teil bekomme ich.


Betrachten Sie nun für meine Frage diese Funktion, die einen bestimmten Betrag in Dollar in Euro umrechnet:

(BEARBEITEN - constIn der ersten Zeile verwenden. Wird letfrüher versehentlich verwendet.)

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Angenommen, wir holen den Wechselkurs von einer Datenbank ab und er ändert sich jeden Tag.

Egal wie oft ich diese Funktion heute aufrufe , sie gibt mir dieselbe Ausgabe für die Eingabe 100. Es könnte mir jedoch morgen eine andere Ausgabe geben. Ich bin nicht sicher, ob dies die erste Bedingung verletzt oder nicht.

IOW, die Funktion selbst enthält keine Logik zum Mutieren der Eingabe, sondern basiert auf einer externen Konstante, die sich in Zukunft möglicherweise ändern wird. In diesem Fall ist es absolut sicher, dass es sich täglich ändert. In anderen Fällen kann es passieren; es könnte nicht.

Können wir solche Funktionen reine Funktionen nennen? Wenn die Antwort NEIN lautet, wie können wir sie dann umgestalten, um eine zu sein?

Schneemann
quelle
6
Die Reinheit einer solch dynamischen Sprache wie JS ist ein sehr kompliziertes Thema:function myNumber(n) { this.n = n; }; myNumber.prototype.valueOf = function() { console.log('impure'); return this.n; }; const n = new myNumber(42); add(n, 1);
Zerkms
29
Reinheit bedeutet, dass Sie den Funktionsaufruf auf Codeebene durch seinen Ergebniswert ersetzen können, ohne das Verhalten Ihres Programms zu ändern.
Bob
1
Weitere Informationen zu
Gilles '
3
Heute ist die Funktion (x) => {return x * 0.9;}. Morgen haben Sie eine andere Funktion, die vielleicht auch rein ist (x) => {return x * 0.89;}. Beachten Sie, dass bei jeder Ausführung (x) => {return x * exchangeRate;}eine neue Funktion erstellt wird und diese Funktion rein ist, da sie exchangeRatesich nicht ändern kann.
user253751
2
Dies ist eine unreine Funktion. Wenn Sie sie rein machen möchten, können Sie sie const dollarToEuro = (x, exchangeRate) => { return x * exchangeRate; }; für eine reine Funktion verwenden. Sie Its return value is the same for the same arguments.sollte immer 1 Sekunde, 1 Jahrzehnt halten. Später, egal was passiert
Vikash Tiwari

Antworten:

133

Der dollarToEuroRückgabewert des 'hängt von einer externen Variablen ab, die kein Argument ist. Daher ist die Funktion unrein.

In der Antwort ist NEIN, wie können wir dann die Funktion so umgestalten, dass sie rein ist?

Eine Möglichkeit ist die Weitergabe exchangeRate. Auf diese Weise (something, somethingElse)wird garantiert , dass jedes Mal, wenn Argumente vorliegen , die Ausgabe wie folgt lautet something * somethingElse:

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Beachten Sie, dass Sie für die funktionale Programmierung Folgendes vermeiden sollten let- immer verwenden const, um eine Neuzuweisung zu vermeiden.

Bestimmte Leistung
quelle
6
Keine freien Variablen zu haben, ist keine Voraussetzung dafür, dass eine Funktion rein ist: const add = x => y => x + y; const one = add(42);Hier sind beide addund onereine Funktionen.
Zerkms
7
const foo = 42; const add42 = x => x + foo;<- Dies ist eine weitere reine Funktion, die wiederum freie Variablen verwendet.
Zerkms
8
@zerkms - Ich würde mich sehr über Ihre Antwort auf diese Frage freuen (auch wenn es nur umformuliert wird, dass CertainPerformance eine andere Terminologie verwendet). Ich denke nicht, dass es duplizieren würde, und es wäre aufschlussreich, besonders wenn es zitiert wird (idealerweise mit besseren Quellen als der obige Wikipedia-Artikel, aber wenn das alles ist, was wir bekommen, immer noch ein Gewinn). (Es wäre leicht, diesen Kommentar in einem negativen Licht zu lesen. Vertrauen Sie mir, dass ich echt bin. Ich denke, eine solche Antwort wäre großartig und würde sie gerne lesen.)
TJ Crowder
17
Ich denke, sowohl Sie als auch @zerkms liegen falsch. Sie scheinen zu denken, dass die dollarToEuroFunktion im Beispiel in Ihrer Antwort unrein ist, da sie von der freien Variablen abhängt exchangeRate. Das ist absurd Wie zerkms betonte, hat die Reinheit einer Funktion nichts damit zu tun, ob sie freie Variablen hat oder nicht. Zerkms ist jedoch auch falsch, weil er glaubt, dass die dollarToEuroFunktion unrein ist, weil sie davon abhängt, exchangeRatewelche aus einer Datenbank stammt. Er sagt, dass es unrein ist, weil "es transitiv vom IO abhängt".
Aadit M Shah
9
(Forts.) Auch das ist absurd, weil es darauf hindeutet, dass dollarToEuroes unrein ist, weil exchangeRatees eine freie Variable ist. Es legt nahe, dass wenn exchangeRatees keine freie Variable wäre, dh wenn es ein Argument dollarToEurowäre, es rein wäre. Daher legt es nahe, dass dies dollarToEuro(100)unrein, aber dollarToEuro(100, exchangeRate)rein ist. Das ist eindeutig absurd, weil Sie in beiden Fällen darauf angewiesen sind, exchangeRatewas aus einer Datenbank stammt. Der einzige Unterschied besteht darin, ob es sich exchangeRateum eine freie Variable innerhalb der dollarToEuroFunktion handelt oder nicht .
Aadit M Shah
76

Technisch gesehen ist jedes Programm, das Sie auf einem Computer ausführen, unrein, da es schließlich zu Anweisungen wie "Diesen Wert verschieben in eax" und "Diesen Wert zum Inhalt von eax" hinzufügen , die unrein sind. Das ist nicht sehr hilfreich.

Stattdessen denken wir über Reinheit mit Black Boxes nach . Wenn ein Code bei gleichen Eingaben immer die gleichen Ausgaben erzeugt, wird er als rein betrachtet. Nach dieser Definition ist die folgende Funktion auch rein, obwohl intern eine unreine Memotabelle verwendet wird.

const fib = (() => {
    const memo = [0, 1];

    return n => {
      if (n >= memo.length) memo[n] = fib(n - 1) + fib(n - 2);
      return memo[n];
    };
})();

console.log(fib(100));

Die Interna sind uns egal, da wir eine Black-Box-Methode zur Überprüfung der Reinheit verwenden. Ebenso ist es uns egal, dass der gesamte Code schließlich in unreine Maschinenanweisungen konvertiert wird, da wir über die Reinheit mithilfe einer Black-Box-Methode nachdenken. Interna sind nicht wichtig.

Betrachten Sie nun die folgende Funktion.

const greet = name => {
    console.log("Hello %s!", name);
};

greet("World");
greet("Snowman");

Ist die greetFunktion rein oder unrein? Wenn wir nach unserer Black-Box-Methode dieselbe Eingabe (z. B. World) geben, wird immer dieselbe Ausgabe auf den Bildschirm gedruckt (dh Hello World!). In diesem Sinne ist es nicht rein? Nein, ist es nicht. Der Grund, warum es nicht rein ist, ist, dass wir das Drucken von etwas auf den Bildschirm als Nebeneffekt betrachten. Wenn unsere Black Box Nebenwirkungen hervorruft, ist sie nicht rein.

Was ist eine Nebenwirkung? Hier ist das Konzept der referenziellen Transparenz nützlich. Wenn eine Funktion referenziell transparent ist, können wir Anwendungen dieser Funktion immer durch ihre Ergebnisse ersetzen. Beachten Sie, dass dies nicht mit dem Inlining von Funktionen identisch ist .

Beim Inlining von Funktionen ersetzen wir Anwendungen einer Funktion durch den Hauptteil der Funktion, ohne die Semantik des Programms zu ändern. Eine referenziell transparente Funktion kann jedoch immer durch ihren Rückgabewert ersetzt werden, ohne die Semantik des Programms zu ändern. Betrachten Sie das folgende Beispiel.

console.log("Hello %s!", "World");
console.log("Hello %s!", "Snowman");

Hier haben wir die Definition von eingefügt greetund die Semantik des Programms nicht geändert.

Betrachten Sie nun das folgende Programm.

undefined;
undefined;

Hier haben wir die Anwendungen der greetFunktion durch ihre Rückgabewerte ersetzt und die Semantik des Programms geändert. Wir drucken keine Grüße mehr auf den Bildschirm. Aus diesem Grund wird das Drucken als Nebeneffekt angesehen, und deshalb ist die greetFunktion unrein. Es ist nicht referenziell transparent.

Betrachten wir nun ein anderes Beispiel. Betrachten Sie das folgende Programm.

const main = async () => {
    const response = await fetch("https://time.akamai.com/");
    const serverTime = 1000 * await response.json();
    const timeDiff = time => time - serverTime;
    console.log("%d ms", timeDiff(Date.now()));
};

main();

Die mainFunktion ist eindeutig unrein. Ist die timeDiffFunktion jedoch rein oder unrein? Obwohl es davon abhängt, serverTimewas von einem unreinen Netzwerkanruf kommt, ist es dennoch referenziell transparent, da es dieselben Ausgaben für dieselben Eingaben zurückgibt und keine Nebenwirkungen hat.

zerkms wird mir in diesem Punkt wahrscheinlich nicht zustimmen. In seiner Antwort sagte er, dass die dollarToEuroFunktion im folgenden Beispiel unrein ist, weil "sie transitiv von der E / A abhängt".

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Ich muss ihm nicht zustimmen, weil die Tatsache, dass die exchangeRateaus einer Datenbank stammen, irrelevant ist. Es ist ein internes Detail und unsere Black-Box-Methode zur Bestimmung der Reinheit einer Funktion kümmert sich nicht um interne Details.

In rein funktionalen Sprachen wie Haskell haben wir eine Notluke zum Ausführen beliebiger E / A-Effekte. Es wird aufgerufen unsafePerformIO, und wie der Name schon sagt, ist es nicht sicher, wenn Sie es nicht richtig verwenden, da es die referenzielle Transparenz beeinträchtigen kann. Wenn Sie jedoch wissen, was Sie tun, ist die Verwendung absolut sicher.

Es wird im Allgemeinen zum Laden von Daten aus Konfigurationsdateien am Anfang des Programms verwendet. Das Laden von Daten aus Konfigurationsdateien ist eine unreine E / A-Operation. Wir möchten jedoch nicht durch die Übergabe der Daten als Eingaben an jede Funktion belastet werden. Wenn wir also verwenden unsafePerformIO, können wir die Daten auf der obersten Ebene laden und alle unsere reinen Funktionen können von den unveränderlichen globalen Konfigurationsdaten abhängen.

Nur weil eine Funktion von Daten abhängt, die aus einer Konfigurationsdatei, einer Datenbank oder einem Netzwerkaufruf geladen wurden, bedeutet dies nicht, dass die Funktion unrein ist.

Betrachten wir jedoch Ihr ursprüngliches Beispiel mit einer anderen Semantik.

let exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Hier gehe ich davon aus, dass es geändert wird, während das Programm ausgeführt wird , da exchangeRatees nicht als definiert constist. Wenn dies der Fall ist, dollarToEurohandelt es sich definitiv um eine unreine Funktion, da bei einer exchangeRateÄnderung die referenzielle Transparenz beeinträchtigt wird.

Wenn die exchangeRateVariable jedoch nicht geändert wird und in Zukunft niemals geändert wird (dh wenn es sich um einen konstanten Wert handelt), letwird die referenzielle Transparenz nicht beeinträchtigt, obwohl sie als definiert ist . In diesem Fall dollarToEuroist in der Tat eine reine Funktion.

Beachten Sie, dass sich der Wert von exchangeRatejedes Mal ändern kann, wenn Sie das Programm erneut ausführen, und die referenzielle Transparenz dadurch nicht beeinträchtigt wird. Die referenzielle Transparenz wird nur unterbrochen, wenn sie sich während der Ausführung des Programms ändert.

Wenn Sie timeDiffbeispielsweise mein Beispiel mehrmals ausführen, erhalten Sie unterschiedliche Werte für serverTimeund daher unterschiedliche Ergebnisse. Da sich der Wert von serverTimenie ändert, während das Programm ausgeführt wird, ist die timeDiffFunktion jedoch rein.

Aadit M Shah
quelle
3
Das war sehr informativ. Vielen Dank. Und ich wollte es constin meinem Beispiel verwenden.
Schneemann
3
Wenn Sie verwenden wollten, constdann ist die dollarToEuroFunktion in der Tat rein. Der Wert von exchangeRatewürde sich nur ändern, wenn Sie das Programm erneut ausführen. In diesem Fall unterscheiden sich der alte und der neue Prozess. Daher wird die referenzielle Transparenz nicht beeinträchtigt. Es ist, als würde man eine Funktion zweimal mit verschiedenen Argumenten aufrufen. Die Argumente können unterschiedlich sein, aber innerhalb der Funktion bleibt der Wert der Argumente konstant.
Aadit M Shah
3
Das klingt nach einer kleinen Relativitätstheorie: Konstanten sind nur relativ konstant, nicht absolut, nämlich relativ zum laufenden Prozess. Klar die einzig richtige Antwort hier. +1.
Bob
5
Ich bin nicht einverstanden mit "ist unrein, weil es sich schließlich zu Anweisungen wie" Diesen Wert in eax verschieben "und" Diesen Wert zum Inhalt von eax hinzufügen "zusammensetzt . Wenn eaxes gelöscht wird - über ein Laden oder ein Löschen - bleibt der Code unabhängig davon deterministisch Was sonst noch passiert und daher rein ist. Ansonsten sehr umfassende Antwort.
3Dave
3
@Bergi: In einer reinen Sprache mit unveränderlichen Werten ist Identität eigentlich irrelevant. Ob zwei Referenzen, die denselben Wert ergeben, zwei Referenzen auf dasselbe Objekt oder auf verschiedene Objekte sind, kann nur beobachtet werden, indem das Objekt durch eine der Referenzen mutiert wird und beobachtet wird, ob sich der Wert auch ändert, wenn er über die andere Referenz abgerufen wird. Ohne Mutation wird Identität irrelevant. (Wie Rich Hickey sagen würde: Identität ist eine Reihe von
Jörg W Mittag
23

Eine Antwort eines Ich-Puristen (wobei "ich" buchstäblich ich bin, da ich denke, dass diese Frage keine einzige formale "richtige" Antwort hat):

In einer so dynamischen Sprache wie JS mit so vielen Möglichkeiten, Patch-Basistypen zu monkeyen oder benutzerdefinierte Typen mithilfe von Funktionen zu erstellen, wie Object.prototype.valueOfes unmöglich ist, anhand einer Betrachtung zu erkennen, ob eine Funktion rein ist, liegt es am Aufrufer, ob sie dies möchten Nebenwirkungen zu erzeugen.

Eine Demo:

const add = (x, y) => x + y;

function myNumber(n) { this.n = n; };
myNumber.prototype.valueOf = function() {
    console.log('impure'); return this.n;
};

const n = new myNumber(42);

add(n, 1); // this call produces a side effect

Eine Antwort von mir-Pragmatiker:

Aus der Definition von Wikipedia

In der Computerprogrammierung ist eine reine Funktion eine Funktion mit den folgenden Eigenschaften:

  1. Der Rückgabewert ist für dieselben Argumente gleich (keine Variation mit lokalen statischen Variablen, nicht lokalen Variablen, veränderlichen Referenzargumenten oder Eingabestreams von E / A-Geräten).
  2. Die Auswertung hat keine Nebenwirkungen (keine Mutation lokaler statischer Variablen, nicht lokaler Variablen, veränderlicher Referenzargumente oder E / A-Streams).

Mit anderen Worten, es ist nur wichtig, wie sich eine Funktion verhält, nicht wie sie implementiert ist. Und solange eine bestimmte Funktion diese beiden Eigenschaften enthält, ist sie rein, unabhängig davon, wie genau sie implementiert wurde.

Nun zu Ihrer Funktion:

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Es ist unrein, weil es die Anforderung 2 nicht qualifiziert: Es hängt transitiv von der E / A ab.

Ich bin damit einverstanden, dass die obige Aussage falsch ist. Weitere Informationen finden Sie in der anderen Antwort: https://stackoverflow.com/a/58749249/251311

Andere relevante Ressourcen:

zerkms
quelle
4
@TJCrowder meals Zerkms, der eine Antwort gibt.
Zerkms
2
Ja, mit Javascript dreht sich alles um Vertrauen, nicht um Garantien
Bob
4
@ Bob ... oder es ist ein blockierender Anruf.
Zerkms
1
@ zerkms - Danke. Nur damit ich zu 100% sicher bin, besteht der Hauptunterschied zwischen Ihnen add42und mir addXlediglich darin, dass mein xgeändert werden kann und Ihr ftnicht geändert werden kann (und daher add42der Rückgabewert nicht abhängig davon variiert ft).
TJ Crowder
5
Ich bin nicht der Meinung, dass die dollarToEuroFunktion in Ihrem Beispiel unrein ist. Ich erklärte, warum ich in meiner Antwort nicht einverstanden bin. stackoverflow.com/a/58749249/783743
Aadit M Shah
14

Wie andere Antworten gesagt haben, ist die Art und Weise, wie Sie implementiert haben dollarToEuro,

let exchangeRate = fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => { return x * exchangeRate; }; 

ist in der Tat rein, da der Wechselkurs nicht aktualisiert wird, während das Programm läuft. Konzeptionell dollarToEuroscheint es jedoch eine unreine Funktion zu sein, da der aktuellste Wechselkurs verwendet wird. Der einfachste Weg , diese Diskrepanz zu erklären ist , dass Sie nicht umgesetzt haben , dollarToEuroaber dollarToEuroAtInstantOfProgramStart.

Der Schlüssel hier ist, dass es mehrere Parameter gibt, die zur Berechnung einer Währungsumrechnung erforderlich sind, und dass eine wirklich reine Version des Generals dollarToEuroalle liefern würde. Die direktesten Parameter sind der umzurechnende USD-Betrag und der Wechselkurs. Da Sie Ihren Wechselkurs jedoch aus veröffentlichten Informationen abrufen möchten, müssen Sie jetzt drei Parameter angeben:

  • Der Geldbetrag, der umgetauscht werden muss
  • Eine historische Autorität, um Wechselkurse zu konsultieren
  • Das Datum, an dem die Transaktion stattgefunden hat (um die historische Autorität zu indizieren)

Die historische Autorität hier ist Ihre Datenbank. Unter der Annahme, dass die Datenbank nicht kompromittiert ist, wird an einem bestimmten Tag immer das gleiche Ergebnis für den Wechselkurs zurückgegeben. Mit der Kombination dieser drei Parameter können Sie also eine vollständig reine, autarke Version des Generals schreiben dollarToEuro, die ungefähr so ​​aussehen könnte:

function dollarToEuro(x, authority, date) {
    const exchangeRate = authority(date);
    return x * exchangeRate;
}

dollarToEuro(100, fetchFromDatabase, Date.now());

Ihre Implementierung erfasst konstante Werte sowohl für die historische Autorität als auch für das Datum der Transaktion zum Zeitpunkt der Erstellung der Funktion - die historische Autorität ist Ihre Datenbank und das erfasste Datum ist das Datum, an dem Sie das Programm starten - alles, was übrig bleibt, ist der Dollarbetrag , die der Anrufer zur Verfügung stellt. Die unreine Version davon dollarToEuroerhält immer den aktuellsten Wert. Im Wesentlichen wird der Datumsparameter implizit verwendet und auf den Zeitpunkt gesetzt, zu dem die Funktion aufgerufen wird. Dies ist nicht einfach, weil Sie die Funktion niemals zweimal mit denselben Parametern aufrufen können.

Wenn Sie eine reine Version davon haben möchten, die dollarToEuroimmer noch den aktuellsten Wert erhalten kann, können Sie die historische Autorität weiterhin binden, aber den Datumsparameter ungebunden lassen und das Datum als Aufruf vom Aufrufer abfragen mit so etwas:

function dollarToEuro(x, date) {
    const exchangeRate = fetchFromDatabase(date);
    return x * exchangeRate;
}

dollarToEuro(100, Date.now());
Der Hansinator
quelle
@Snowman Gern geschehen! Ich habe die Antwort ein wenig aktualisiert, um weitere Codebeispiele hinzuzufügen.
TheHansinator
8

Ich möchte ein wenig von den spezifischen Details von JS und der Abstraktion formaler Definitionen zurücktreten und darüber sprechen, welche Bedingungen gelten müssen, um bestimmte Optimierungen zu ermöglichen. Das ist normalerweise die Hauptsache, die uns beim Schreiben von Code wichtig ist (obwohl es auch hilft, die Richtigkeit zu beweisen). Funktionale Programmierung ist weder ein Leitfaden für die neuesten Moden noch ein klösterliches Gelübde der Selbstverleugnung. Es ist ein Werkzeug, um Probleme zu lösen.

Wenn Sie Code wie diesen haben:

let exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Wenn exchangeRatezwischen den beiden Aufrufen von nie geändert werden konnte dollarToEuro(100), ist es möglich, das Ergebnis des ersten Aufrufs zu speichern dollarToEuro(100)und den zweiten Anruf zu optimieren. Das Ergebnis ist das gleiche, sodass wir uns nur an den Wert von zuvor erinnern können.

Das exchangeRatekann einmal festgelegt werden, bevor eine Funktion aufgerufen wird, die es nachschlägt, und niemals geändert werden. Weniger restriktiv ist, dass Sie möglicherweise Code haben, der exchangeRateeinmal nach einer bestimmten Funktion oder einem bestimmten Codeblock sucht und in diesem Bereich konsistent denselben Wechselkurs verwendet. Wenn nur dieser Thread die Datenbank ändern kann, können Sie davon ausgehen, dass niemand anderes den Wechselkurs geändert hat, wenn Sie ihn nicht aktualisiert haben.

Wenn fetchFromDatabase()es sich selbst um eine reine Funktion handelt, die zu einer Konstanten ausgewertet wird und exchangeRateunveränderlich ist, können wir diese Konstante während der gesamten Berechnung falten. Ein Compiler, der weiß, dass dies der Fall ist, kann denselben Abzug wie im Kommentar vornehmen, der dollarToEuro(100)90.0 ergibt, und den gesamten Ausdruck durch die Konstante 90.0 ersetzen.

Wenn fetchFromDatabase()jedoch keine E / A ausgeführt wird, was als Nebeneffekt angesehen wird, verstößt der Name gegen das Prinzip des geringsten Erstaunens.

Davislor
quelle
8

Diese Funktion ist nicht rein, sondern basiert auf einer externen Variablen, die sich mit ziemlicher Sicherheit ändern wird.

Die Funktion schlägt daher beim ersten von Ihnen gemachten Punkt fehl. Sie gibt nicht denselben Wert zurück, wenn für dieselben Argumente.

Um diese Funktion "rein" zu machen, geben Sie sie exchangeRateals Argument ein.

Dies würde dann beide Bedingungen erfüllen.

  1. Es würde immer den gleichen Wert zurückgeben, wenn der gleiche Wert und der gleiche Wechselkurs übergeben werden.
  2. Es hätte auch keine Nebenwirkungen.

Beispielcode:

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

dollarToEuro(100, fetchFromDatabase())
Jessica
quelle
1
"was sich fast definitiv ändern wird" --- es ist nicht, es ist const.
Zerkms
7

Um die Punkte zu erweitern, die andere zur referenziellen Transparenz gemacht haben: Wir können Reinheit einfach als referenzielle Transparenz von Funktionsaufrufen definieren (dh jeder Aufruf der Funktion kann durch den Rückgabewert ersetzt werden, ohne die Semantik des Programms zu ändern).

Die beiden Eigenschaften, die Sie angeben, sind beide Konsequenzen der referenziellen Transparenz. Die folgende Funktion f1ist beispielsweise unrein, da sie nicht jedes Mal das gleiche Ergebnis liefert (die Eigenschaft, die Sie mit 1 nummeriert haben):

function f1(x, y) {
  if (Math.random() > 0.5) { return x; }
  return y;
}

Warum ist es wichtig, jedes Mal das gleiche Ergebnis zu erzielen? Weil das Erhalten unterschiedlicher Ergebnisse eine Möglichkeit für einen Funktionsaufruf ist, eine andere Semantik als ein Wert zu haben und somit die referenzielle Transparenz zu brechen.

Angenommen, wir schreiben den Code f1("hello", "world"), führen ihn aus und erhalten den Rückgabewert "hello". Wenn wir jeden Aufruf suchen / ersetzen f1("hello", "world")und durch ersetzen, haben "hello"wir die Semantik des Programms geändert (alle Aufrufe werden jetzt durch ersetzt "hello", aber ursprünglich hätte ungefähr die Hälfte von ihnen ausgewertet "world"). Daher sind Aufrufe f1nicht referenziell transparent, daher f1unrein.

Eine andere Möglichkeit, wie ein Funktionsaufruf eine andere Semantik als ein Wert haben kann, besteht darin, Anweisungen auszuführen. Zum Beispiel:

function f2(x) {
  console.log("foo");
  return x;
}

Der Rückgabewert von f2("bar")wird immer sein "bar", aber die Semantik des Werts "bar"unterscheidet sich vom Aufruf, f2("bar")da dieser auch an der Konsole protokolliert. Das Ersetzen durch das andere würde die Semantik des Programms ändern, so dass es nicht referenziell transparent und daher f2unrein ist.

Ob Ihre dollarToEuroFunktion referenziell transparent (und damit rein) ist, hängt von zwei Dingen ab:

  • Der "Umfang" dessen, was wir als referenziell transparent betrachten
  • Ob sich der exchangeRateWille jemals in diesem Bereich ändern wird?

Es gibt keinen "besten" Anwendungsbereich. Normalerweise würden wir über einen einzelnen Programmlauf oder die Lebensdauer des Projekts nachdenken. Stellen Sie sich als Analogie vor, dass die Rückgabewerte jeder Funktion zwischengespeichert werden (wie in der Memotabelle im Beispiel von @ aadit-m-shah): Wann müssten wir den Cache leeren, um sicherzustellen, dass veraltete Werte unsere nicht beeinträchtigen Semantik?

Wenn exchangeRateverwendet var, könnte es zwischen jedem Aufruf von ändern dollarToEuro; Wir müssten alle zwischengespeicherten Ergebnisse zwischen jedem Aufruf löschen, damit keine referenzielle Transparenz zu sprechen wäre.

Durch die Verwendung erweitern constwir den 'Bereich' auf einen Programmlauf: Es wäre sicher, Rückgabewerte von zwischenzuspeichern, dollarToEurobis das Programm beendet ist. Wir könnten uns vorstellen, ein Makro (in einer Sprache wie Lisp) zu verwenden, um Funktionsaufrufe durch ihre Rückgabewerte zu ersetzen. Diese Reinheit wird häufig für Konfigurationswerte, Befehlszeilenoptionen oder eindeutige IDs verwendet. Wenn wir uns darauf beschränken, über einen Programmlauf nachzudenken, erhalten wir die meisten Vorteile der Reinheit, müssen jedoch über alle Läufe hinweg vorsichtig sein (z. B. Daten in einer Datei speichern und dann in einem anderen Lauf laden). Ich würde solche Funktionen nicht abstrakt als "rein" bezeichnen (z. B. wenn ich eine Wörterbuchdefinition schreibe), aber ich habe kein Problem damit, sie im Kontext als rein zu behandeln .

Wenn wir die Lebensdauer des Projekts als unseren "Umfang" betrachten, sind wir die "referenziell transparentesten" und damit die "reinsten", selbst im abstrakten Sinne. Wir würden niemals unseren hypothetischen Cache leeren müssen. Wir könnten dieses "Caching" sogar durchführen, indem wir den Quellcode direkt auf die Festplatte schreiben, um Aufrufe durch ihre Rückgabewerte zu ersetzen. Dies würde auch Arbeit in Projekten, zum Beispiel könnten wir eine Online - Datenbank von Funktionen und deren Rückgabewerte vorstellen, wo jemand einen Funktionsaufruf nachschlagen kann und (wenn es in der DB ist) den Rückgabewert verwenden , indem Sie jemand auf der anderen Seite der bereitgestellten Welt, die vor Jahren eine identische Funktion für ein anderes Projekt verwendet hat.

Warbo
quelle
4

Wie geschrieben, ist es eine reine Funktion. Es entstehen keine Nebenwirkungen. Die Funktion hat einen formalen Parameter, aber zwei Eingänge und gibt für zwei Eingänge immer den gleichen Wert aus.

11112222233333
quelle
2

Können wir solche Funktionen reine Funktionen nennen? Wenn die Antwort NEIN lautet, wie können wir sie dann umgestalten, um eine zu sein?

Wie Sie ordnungsgemäß bemerkt haben, "könnte es mir morgen eine andere Ausgabe geben" . Sollte dies der Fall sein, würde die Antwort ein klares "Nein" sein . Dies gilt insbesondere dann, wenn Ihr beabsichtigtes Verhalten dollarToEurokorrekt interpretiert wurde als:

const dollarToEuro = (x) => {
  const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;
  return x * exchangeRate;
};

Es gibt jedoch eine andere Interpretation, bei der sie als rein angesehen wird:

const dollarToEuro = ( () => {
    const exchangeRate =  fetchFromDatabase();

    return ( x ) => x * exchangeRate;
} )();

dollarToEuro direkt darüber ist rein.


Aus Sicht der Softwareentwicklung ist es wichtig, die Abhängigkeit von dollarToEuroder Funktion zu deklarieren fetchFromDatabase. Überarbeiten Sie daher die Definition dollarToEurowie folgt:

const dollarToEuro = ( x, fetchFromDatabase ) => {
  return x * fetchFromDatabase();
};

Mit diesem Ergebnis fetchFromDatabasekönnen wir unter der Voraussetzung, dass zufriedenstellend funktioniert, schließen, dass die Projektion von fetchFromDatabaseon dollarToEurozufriedenstellend sein muss. Oder die Aussage " fetchFromDatabaseist rein" impliziert dollarToEuroist rein (da fetchFromDatabaseist eine Basis für dollarToEuroden Skalarfaktor von x.

Aus dem ursprünglichen Beitrag kann ich verstehen, dass dies fetchFromDatabaseeine Funktionszeit ist. Lassen Sie uns die Refactoring-Bemühungen verbessern, um dieses Verständnis transparent zu machen und somit eindeutig fetchFromDatabaseals reine Funktion zu qualifizieren:

fetchFromDatabase = (Zeitstempel) => {/ * hier geht die Implementierung * /};

Letztendlich würde ich das Feature wie folgt umgestalten:

const fetchFromDatabase = ( timestamp ) => { /* here goes the implementation */ };

// Do a partial application of `fetchFromDatabase` 
const exchangeRate = fetchFromDatabase.bind( null, Date.now() );

const dollarToEuro = ( dollarAmount, exchangeRate ) => dollarAmount * exchangeRate();

Folglich dollarToEurokann ein Unit-Test durchgeführt werden, indem einfach nachgewiesen wird, dass es korrekt aufruft fetchFromDatabase(oder seine Ableitung exchangeRate).

Igwe Kalu
quelle
1
Das war sehr aufschlussreich. +1. Vielen Dank.
Schneemann
Während ich Ihre Antwort informativer finde und vielleicht das bessere Refactoring für den speziellen Anwendungsfall von dollarToEuro; Ich habe es im OP erwähnt, dass es andere Anwendungsfälle geben könnte. Ich habe mich für dollarToEuro entschieden, weil es sofort an das erinnert, was ich versuche, aber es könnte etwas weniger Feines geben, das von einer freien Variablen abhängt, die sich ändern kann, aber nicht unbedingt als Funktion der Zeit. In diesem Sinne finde ich, dass der am besten bewertete Refaktor der zugänglichere ist und derjenige, der anderen bei ähnlichen Anwendungsfällen helfen kann. Trotzdem für Ihre Hilfe.
Schneemann
-1

Ich bin zweisprachig in Haskell / JS und Haskell ist eine der Sprachen, die einen großen Einfluss auf die Funktionsreinheit haben. Deshalb dachte ich, ich würde Ihnen die Perspektive geben, wie Haskell sie sieht.

Wie andere gesagt haben, wird das Lesen einer veränderlichen Variablen in Haskell im Allgemeinen als unrein angesehen. Es gibt einen Unterschied zwischen Variablen und Definitionen darin, dass sich Variablen später ändern können. Definitionen sind für immer gleich. Also wenn du es also deklariert hätten const(vorausgesetzt, es ist nur eine numberund hat keine veränderbare interne Struktur), würde das Lesen daraus eine Definition verwenden, die rein ist. Aber Sie wollten Wechselkurse modellieren, die sich im Laufe der Zeit ändern, und das erfordert eine gewisse Veränderlichkeit, und dann geraten Sie in Unreinheit.

Um diese Art von unreinen Dingen in Haskell zu beschreiben (wir können sie als „Effekte“ bezeichnen und ihre Verwendung als „effektiv“ im Gegensatz zu „rein“), führen wir das aus, was Sie als Metaprogrammierung bezeichnen könnten . Heutzutage bezieht sich Metaprogrammierung normalerweise auf Makros , was ich nicht meine, sondern nur auf die Idee, ein Programm zu schreiben, um ein anderes Programm im Allgemeinen zu schreiben.

In diesem Fall schreiben wir in Haskell eine reine Berechnung, die ein effektives Programm berechnet, das dann tut, was wir wollen. Der springende Punkt einer Haskell-Quelldatei (zumindest eine, die ein Programm beschreibt, nicht eine Bibliothek) besteht darin, eine reine Berechnung für ein effektives Programm zu beschreiben, das void erzeugtmain . Dann besteht die Aufgabe des Haskell-Compilers darin, diese Quelldatei zu nehmen, diese reine Berechnung durchzuführen und dieses effektive Programm als ausführbare Binärdatei irgendwo auf Ihrer Festplatte abzulegen, um es später nach Belieben auszuführen. Mit anderen Worten, es gibt eine Lücke zwischen dem Zeitpunkt, zu dem die reine Berechnung ausgeführt wird (während der Compiler die ausführbare Datei erstellt) und dem Zeitpunkt, zu dem das effektive Programm ausgeführt wird (wann immer Sie die ausführbare Datei ausführen).

Effektive Programme sind für uns also wirklich eine Datenstruktur und sie tun an sich nichts, indem sie nur erwähnt werden (sie haben zusätzlich zu ihrem Rückgabewert keine * Nebenwirkungen *; ihr Rückgabewert enthält ihre Auswirkungen). Für ein sehr leichtes Beispiel einer TypeScript-Klasse, die unveränderliche Programme und einige Dinge beschreibt, die Sie damit tun können,

export class Program<x> {
   // wrapped function value
   constructor(public run: () => Promise<x>) {}
   // promotion of any value into a program which makes that value
   static of<v>(value: v): Program<v> {
     return new Program(() => Promise.resolve(value));
   }
   // applying any pure function to a program which makes its input
   map<y>(fn: (x: x) => y): Program<y> {
     return new Program(() => this.run().then(fn));
   }
   // sequencing two programs together
   chain<y>(after: (x: x) => Program<y>): Program<y> {
    return new Program(() => this.run().then(x => after(x).run()));
   }
}

Der Schlüssel ist, wenn Sie eine haben Program<x> keine Nebenwirkungen aufgetreten sind und dies völlig funktional reine Einheiten sind. Das Zuordnen einer Funktion über ein Programm hat keine Nebenwirkungen, es sei denn, die Funktion war keine reine Funktion. Die Sequenzierung von zwei Programmen hat keine Nebenwirkungen. usw.

Wenn Sie dies beispielsweise in Ihrem Fall anwenden möchten, können Sie einige reine Funktionen schreiben, die Programme zurückgeben, um Benutzer anhand ihrer ID abzurufen, eine Datenbank zu ändern und JSON-Daten abzurufen, z

// assuming a database library in knex, say
function getUserById(id: number): Program<{ id: number, name: string, supervisor_id: number }> {
    return new Program(() => knex.select('*').from('users').where({ id }));
}
function notifyUserById(id: number, message: string): Program<void> {
    return new Program(() => knex('messages').insert({ user_id: id, type: 'notification', message }));
}
function fetchJSON(url: string): Program<any> {
  return new Program(() => fetch(url).then(response => response.json()));
}

und dann könnten Sie einen Cron-Job beschreiben, um eine URL zu kräuseln und einen Mitarbeiter nachzuschlagen und seinen Vorgesetzten auf rein funktionale Weise zu benachrichtigen

const action =
  fetchJSON('http://myapi.example.com/employee-of-the-month')
    .chain(eotmInfo => getUserById(eotmInfo.id))
    .chain(employee => 
        getUserById(employee.supervisor_id)
          .chain(supervisor => notifyUserById(
            supervisor.id,
            'Your subordinate ' + employee.name + ' is employee of the month!'
          ))
    );

Der Punkt ist, dass jede einzelne Funktion hier eine völlig reine Funktion ist; nichts ist tatsächlich passiert, bis ich es tatsächlich action.run()in Bewegung gesetzt habe. Außerdem kann ich Funktionen schreiben wie:

// do two things in parallel
function parallel<x, y>(x: Program<x>, y: Program<y>): Program<[x, y]> {
    return new Program(() => Promise.all([x.run(), y.run()]));
}

und wenn JS eine Stornierung des Versprechens hätte, könnten zwei Programme gegeneinander antreten und das erste Ergebnis nehmen und das zweite stornieren. (Ich meine, wir können es immer noch, aber es wird weniger klar, was zu tun ist.)

Ebenso können wir in Ihrem Fall Wechselkursänderungen mit beschreiben

declare const exchangeRate: Program<number>;

function dollarsToEuros(dollars: number): Program<number> {
  return exchangeRate.map(rate => dollars * rate);
}

und exchangeRatekönnte ein Programm sein, das einen veränderlichen Wert betrachtet,

let privateExchangeRate: number = 0;
export function setExchangeRate(value: number): Program<void> {
  return new Program(() => { privateExchangeRate = value; return Promise.resolve(undefined); });
}
export const exchangeRate: Program<number> = new Program(() => {
  return Promise.resolve(privateExchangeRate); 
});

aber trotzdem diese Funktion dollarsToEuros jetzt eine reine Funktion von einer Zahl zu einem Programm, das eine Zahl erzeugt, und Sie können auf diese deterministische Art und Weise darüber argumentieren, wie Sie über jedes Programm argumentieren können, das keine Nebenwirkungen hat.

Die Kosten sind natürlich, dass Sie das irgendwann .run() irgendwo nennen müssen , und das wird unrein sein. Die gesamte Struktur Ihrer Berechnung kann jedoch durch eine reine Berechnung beschrieben werden, und Sie können die Verunreinigung an den Rand Ihres Codes verschieben.

CR Drost
quelle
Ich bin neugierig, warum dies immer wieder herabgestuft wird, aber ich meine, ich stehe immer noch dazu (es ist in der Tat, wie Sie Programme in Haskell manipulieren, bei denen die Dinge standardmäßig rein sind) und werde die Downvotes gerne tanken. Wenn Downvoter jedoch Kommentare hinterlassen möchten, in denen erklärt wird, was ihnen daran nicht gefällt, kann ich versuchen, es zu verbessern.
CR Drost
Ja, ich habe mich gefragt, warum es so viele Abstimmungen gibt, aber keinen einzigen Kommentar, außer natürlich den Autor.
Buda Örs