javascript - Ist das eine reine Funktion?




function functional-programming (7)

Die meisten sources 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. Betrachten 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 für nachfolgende Aufrufe unterschiedliche Ausgaben liefert, wodurch die erste Bedingung verletzt wird. Und daher ist es unrein.

Diesen Teil bekomme ich.

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

(BEARBEITEN - Verwenden von const in der ersten Zeile. Wird versehentlich früher 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

Nehmen wir an, wir holen den Wechselkurs aus einer Datenbank und er ändert sich jeden Tag.

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

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

Können wir solche Funktionen reine Funktionen nennen? Wenn die Antwort NEIN ist, wie können wir es dann umgestalten, um eins zu sein?


Können wir solche Funktionen reine Funktionen nennen? Wenn die Antwort NEIN ist, wie können wir es dann umgestalten, um eins zu sein?

Wie Sie zur Kenntnis genommen, „es könnte mir morgen einen anderen Ausgang geben“. Sollte dies der Fall sein, wäre die Antwort „nein“ einen durchschlagenden. Dies gilt insbesondere dann, wenn Ihr beabsichtigtes Verhalten von dollarToEuro korrekt 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, wenn dies als rein angesehen werden würde:

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 dollarToEuro von der Funktion fetchFromDatabase . dollarToEuro daher die Definition von dollarToEuro wie folgt:

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

Unter der Voraussetzung, dass fetchFromDatabase zufriedenstellend funktioniert, können wir daraus schließen, dass die Projektion von fetchFromDatabase auf dollarToEuro zufriedenstellend dollarToEuro muss. Oder die Anweisung " fetchFromDatabase ist rein" impliziert, dass dollarToEuro rein ist (da fetchFromDatabase eine Basis für dollarToEuro durch den dollarToEuro von x .

Aus dem ursprünglichen Beitrag kann ich verstehen, dass fetchFromDatabase eine Funktionszeit ist. Verbessern wir den Refactoring-Aufwand, um dieses Verständnis transparent zu machen und fetchFromDatabase daher eindeutig als reine Funktion zu qualifizieren:

fetchFromDatabase = (timestamp) => {/ * 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 kann dollarToEuro Unit-getestet werden, indem einfach nachgewiesen wird, dass es fetchFromDatabase (oder seine abgeleitete exchangeRate ) korrekt aufruft.


Die dollarToEuro ‚s Rückgabewert ist abhängig von einer externen Variable, die kein Argument ist, so ist es unrein.

In der Antwort ist NEIN, wie können wir es dann umgestalten, um eins zu sein?

Eine Möglichkeit wäre die Weitergabe von exchangeRate . Auf diese Weise wird jedes Mal, wenn die Argumente (something, somethingElse) , garantiert , dass die Ausgabe genau 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 auf jeden Fall auch nicht let - verwenden Sie immer const , um eine Neuzuweisung zu vermeiden.


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 Object.prototype.valueOf oder benutzerdefinierte Typen mit Funktionen wie Object.prototype.valueOf es unmöglich zu erkennen, ob eine Funktion rein ist Anrufer an, ob sie Nebenwirkungen hervorrufen wollen.

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 des Pragmatikers:

Von der sources

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 von lokalen statischen Variablen, nicht lokalen Variablen, veränderlichen Referenzargumenten oder E / A-Strömen).

Mit anderen Worten, es kommt nur darauf an, wie sich eine Funktion verhält, nicht darauf, 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 erfüllt: es hängt transitiv vom IO ab.

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

Andere relevante Ressourcen:


Ich möchte mich ein wenig von den spezifischen Details von JS und der Abstraktion formaler Definitionen zurückziehen und darüber sprechen, welche Bedingungen erfüllt sein müssen, um spezifische Optimierungen zu ermöglichen. Das ist normalerweise die Hauptsache, die uns beim Schreiben von Code wichtig ist (obwohl dies auch zum Nachweis der Korrektheit beiträgt). 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 exchangeRate zwischen den beiden Aufrufen von dollarToEuro(100) niemals geändert werden konnte, ist es möglich, das Ergebnis des ersten Aufrufs von dollarToEuro(100) zu dollarToEuro(100) und den zweiten Aufruf zu optimieren. Das Ergebnis wird das gleiche sein, so können wir nur den Wert erinnern, aus der Zeit vor.

Die exchangeRate kann vor dem Aufrufen einer Funktion, die sie nachschlägt, einmal festgelegt und niemals geändert werden. Weniger restriktiv könnte es sein, dass Sie Code haben, der die exchangeRate einmal für eine bestimmte Funktion oder einen bestimmten Codeblock nachschlägt und in diesem Bereich konsistent denselben Wechselkurs verwendet. Oder wenn nur dieser Thread die Datenbank ändern kann, können Sie davon ausgehen, dass, wenn Sie den Wechselkurs nicht aktualisiert haben, niemand anderes ihn an Ihnen geändert hat.

Wenn fetchFromDatabase() ist selbst eine reine Funktion einer konstanten Bewertung und exchangeRate ist unveränderlich, konnten wir diese Konstante den ganzen Weg durch die Berechnung falten. Ein Compiler, der weiß, dass dies der Fall ist, kann dieselbe Schlussfolgerung ziehen wie im Kommentar, dass dollarToEuro(100) 90.0 dollarToEuro(100) , und den gesamten Ausdruck durch die Konstante 90.0 ersetzen.

Wenn jedoch fetchFromDatabase() nicht durchführen I / O, die ein Nebeneffekt betrachtet wird, verletzt seinen Namen dem Prinzip der geringsten Astonishment.


Um die Punkte zu erweitern, die andere über referenzielle Transparenz angesprochen 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 Konsequenzen der referenziellen Transparenz. Die folgende Funktion f1 ist beispielsweise unrein, da sie nicht jedes Mal dasselbe 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? Da es eine Möglichkeit ist, unterschiedliche Ergebnisse zu erhalten, kann ein Funktionsaufruf eine andere Semantik als ein Wert haben und damit die referenzielle Transparenz aufheben.

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

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

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

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

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

  • Der "Umfang" dessen, was wir als referenziell transparent betrachten
  • Ob sich die exchangeRate 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 die Memo-Tabelle im Beispiel von @ aadit-m-shah): Wann müssten wir den Cache leeren, um sicherzustellen, dass veraltete Werte unsere nicht beeinträchtigen Semantik?

Wenn exchangeRate var es sich zwischen jedem Aufruf von dollarToEuro . Zwischen jedem Aufruf müssten die zwischengespeicherten Ergebnisse gelöscht werden, sodass keine nennenswerte referenzielle Transparenz vorhanden wäre.

Mit const wir den 'scope' auf einen Programmlauf: Es wäre sicher, die Rückgabewerte von dollarToEuro bis 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 Menge an Reinheit ist für Dinge wie Konfigurationswerte, Befehlszeilenoptionen oder eindeutige IDs üblich. Wenn wir uns darauf beschränken, über einen Programmlauf nachzudenken, erhalten wir die meisten Vorteile der Reinheit, aber wir müssen über 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 "Geltungsbereich" betrachten, dann sind wir die "referenziell transparentesten" und damit die "reinsten", auch im abstrakten Sinne. Wir müssten niemals unseren hypothetischen Cache leeren. 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 sogar projektübergreifend funktionieren, z. B. könnten wir uns eine Online-Datenbank mit Funktionen und ihren Rückgabewerten vorstellen, in der jeder einen Funktionsaufruf nachschlagen und (wenn er sich in der Datenbank befindet) den Rückgabewert verwenden kann, der von jemandem auf der anderen Seite der Datenbank bereitgestellt wird Welt, die vor Jahren eine identische Funktion für ein anderes Projekt verwendet hat.


Wie andere Antworten bereits sagten, wie Sie dollarToEuro implementiert dollarToEuro ,

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

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

in der Tat rein ist, weil der Wechselkurs nicht mehr aktualisiert wird, während das Programm läuft. Konzeptionell scheint es sich bei dollarToEuro jedoch um eine unreine Funktion zu handeln, da der aktuellste Wechselkurs verwendet wird. Der einfachste Weg, um diese Diskrepanz zu erklären, besteht darin, dass Sie nicht dollarToEuro sondern dollarToEuroAtInstantOfProgramStart implementiert 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 allgemeinen dollarToEuro alle dollarToEuro würde. Die direkteste Parameter sind die Höhe von USD zu konvertieren, und der Wechselkurs. Da Sie jedoch Ihren Wechselkurs aus veröffentlichten Informationen erhalten möchten, müssen Sie jetzt drei Parameter angeben:

  • Der Geldbetrag, den Sie umtauschen möchten
  • Eine historische Instanz, um Wechselkurse zu erfragen
  • Das Datum, an dem die Transaktion stattgefunden hat (um die historische Autorität zu indizieren)

Die historische Instanz ist hier Ihre Datenbank. Unter der Annahme, dass die Datenbank nicht gefährdet ist, wird immer das gleiche Ergebnis für den Wechselkurs an einem bestimmten Tag zurückgegeben. Daher können Sie mit der Kombination dieser drei Parameter eine vollständig reine, autarke Version des allgemeinen dollarToEuro , die dollarToEuro aussehen könnte:

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

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

Die Implementierung erfasst zum Zeitpunkt der Erstellung der Funktion konstante Werte sowohl für die historische Berechtigung als auch für das Datum der Transaktion. Die historische Berechtigung 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 bereitstellt. Die unreine Version von dollarToEuro , die immer den aktuellsten Wert erhält, nimmt den dollarToEuro Wesentlichen implizit und setzt ihn auf den Zeitpunkt, an dem die Funktion aufgerufen wird. dollarToEuro ist nicht einfach so, weil Sie die Funktion niemals zweimal mit denselben Parametern aufrufen können .

Wenn Sie eine reine Version von dollarToEuro haben dollarToEuro , die immer noch den aktuellsten Wert dollarToEuro , können Sie die historische Berechtigung weiterhin binden, den dollarToEuro jedoch ungebunden lassen und den Aufrufer nach dem Datum fragen, das als Argument endet mit so etwas auf:

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

dollarToEuro(100, Date.now());

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





functional-programming