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 number
und 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 exchangeRate
kö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.
function myNumber(n) { this.n = n; }; myNumber.prototype.valueOf = function() { console.log('impure'); return this.n; }; const n = new myNumber(42); add(n, 1);