alias:: Transducer: Ein leistungsstarkes Funktionskompositionsmuster
Notizbuch:: Wandler: 一种强大的函数组合模式
Die Semantik von Map ist „Mapping“, was bedeutet, dass eine Transformation für alle Elemente in einer Menge einmal durchgeführt wird.
const list = [1, 2, 3, 4, 5] list.map(x => x + 1) // [ 2, 3, 4, 5, 6 ]
function map(f, xs) { const ret = [] for (let i = 0; i < xs.length; i++) { ret.push(f(xs[i])) } return ret }
map(x => x + 1, [1, 2, 3, 4, 5]) // [ 2, 3, 4, 5, 6 ]
Oben wird absichtlich eine for-Anweisung verwendet, um klar zum Ausdruck zu bringen, dass die Implementierung von Map vom Sammlungstyp abhängt.
Sequentielle Ausführung;
Sofortige Bewertung, nicht faul.
Schauen wir uns den Filter an:
function filter(f, xs) { const ret = [] for (let i = 0; i < xs.length; i++) { if (f(xs[i])) { ret.push(xs[i]) } } return ret }
var range = n => [...Array(n).keys()]
filter(x => x % 2 === 1, range(10)) // [ 1, 3, 5, 7, 9 ]
In ähnlicher Weise hängt die Implementierung von Filter auch vom spezifischen Sammlungstyp ab, und die aktuelle Implementierung erfordert, dass xs ein Array ist.
Wie kann die Karte verschiedene Datentypen unterstützen? Zum Beispiel „Set“, „Map“ und benutzerdefinierte Datentypen.
Es gibt einen herkömmlichen Weg: Er basiert auf der Schnittstelle (Protokoll) der Sammlung.
Unterschiedliche Sprachen haben unterschiedliche Implementierungen. JS bietet in dieser Hinsicht relativ schwache native Unterstützung, aber es ist auch machbar:
Iterieren Sie mit Symbol.iterator.
Verwenden Sie „Object#contractor“, um den Konstruktor zu erhalten.
Wie unterstützen wir also abstrakt verschiedene Datentypen in Push?
Sie imitiert die ramdajs-Bibliothek und kann auf die benutzerdefinierte @@transducer/step-Funktion zurückgreifen.
function map(f, xs) { const ret = new xs.constructor() // 1. construction for (const x of xs) { // 2. iteration ret['@@transducer/step'](f(x)) // 3. collection } return ret }
Array.prototype['@@transducer/step'] = Array.prototype.push // [Function: push]
map(x => x + 1, [1, 2, 3, 4, 5]) // [ 2, 3, 4, 5, 6 ]
Set.prototype['@@transducer/step'] = Set.prototype.add // [Function: add]
map(x => x + 1, new Set([1, 2, 3, 4, 5])) // Set (5) {2, 3, 4, 5, 6}
Mit dieser Methode können wir Funktionen wie Map , Filter usw. implementieren, die eher axial sind.
Der Schlüssel besteht darin, Vorgänge wie Konstruktion, Iteration und Sammlung an bestimmte Sammlungsklassen zu delegieren, da nur die Sammlung selbst weiß, wie diese Vorgänge ausgeführt werden.
function filter(f, xs) { const ret = new xs.constructor() for (const x of xs) { if (f(x)) { ret['@@transducer/step'](x) } } return ret }
filter(x => x % 2 === 1, range(10)) // [ 1, 3, 5, 7, 9 ]
filter(x => x > 3, new Set(range(10))) // Set (6) {4, 5, 6, 7, 8, 9}
Es treten einige Probleme auf, wenn die obige Karte und der obige Filter in Kombination verwendet werden.
range(10) .map(x => x + 1) .filter(x => x % 2 === 1) .slice(0, 3) // [ 1, 3, 5 ]
Obwohl nur 5 Elemente verwendet werden, werden alle Elemente in der Sammlung durchlaufen.
Bei jedem Schritt wird ein Zwischensammlungsobjekt generiert.
Wir verwenden Compose, um diese Logik erneut zu implementieren
function compose(...fns) { return fns.reduceRight((acc, fn) => x => fn(acc(x)), x => x) }
Um die Komposition zu unterstützen, implementieren wir Funktionen wie „Karte“ und „Filter“ in Form von „Curry“.
function curry(f) { return (...args) => data => f(...args, data) }
var rmap = curry(map) var rfilter = curry(filter) function take(n, xs) { const ret = new xs.constructor() for (const x of xs) { if (n <= 0) { break } n-- ret['@@transducer/step'](x) } return ret } var rtake = curry(take)
take(3, range(10)) // [ 0, 1, 2 ]
take(4, new Set(range(10))) // Set (4) {0, 1, 2, 3}
const takeFirst3Odd = compose( rtake(3), rfilter(x => x % 2 === 1), rmap(x => x + 1) ) takeFirst3Odd(range(10)) // [ 1, 3, 5 ]
Bisher ist unsere Implementierung klar und prägnant im Ausdruck, aber verschwenderisch in der Laufzeit.
Die Kartenfunktion in der Version Curry sieht folgendermaßen aus:
const map = f => xs => ...
Das heißt, map(x => ...) gibt eine Einzelparameterfunktion zurück.
const list = [1, 2, 3, 4, 5] list.map(x => x + 1) // [ 2, 3, 4, 5, 6 ]
Funktionen mit einem einzigen Parameter können einfach zusammengestellt werden.
Insbesondere sind die Eingaben dieser Funktionen „Daten“, die Ausgabe sind die verarbeiteten Daten und die Funktion ist ein Datentransformator (Transformer).
function map(f, xs) { const ret = [] for (let i = 0; i < xs.length; i++) { ret.push(f(xs[i])) } return ret }
map(x => x + 1, [1, 2, 3, 4, 5]) // [ 2, 3, 4, 5, 6 ]
function filter(f, xs) { const ret = [] for (let i = 0; i < xs.length; i++) { if (f(xs[i])) { ret.push(xs[i]) } } return ret }
Transformer ist eine Einzelparameterfunktion, die sich für die Funktionskomposition eignet.
var range = n => [...Array(n).keys()]
Ein Reduzierer ist eine Zwei-Parameter-Funktion, die verwendet werden kann, um komplexere Logik auszudrücken.
filter(x => x % 2 === 1, range(10)) // [ 1, 3, 5, 7, 9 ]
function map(f, xs) { const ret = new xs.constructor() // 1. construction for (const x of xs) { // 2. iteration ret['@@transducer/step'](f(x)) // 3. collection } return ret }
Array.prototype['@@transducer/step'] = Array.prototype.push // [Function: push]
map(x => x + 1, [1, 2, 3, 4, 5]) // [ 2, 3, 4, 5, 6 ]
Set.prototype['@@transducer/step'] = Set.prototype.add // [Function: add]
Wie implementiert man Take? Dies erfordert „reduce“, um eine ähnliche Funktionalität wie „break“ zu haben.
map(x => x + 1, new Set([1, 2, 3, 4, 5])) // Set (5) {2, 3, 4, 5, 6}
function filter(f, xs) { const ret = new xs.constructor() for (const x of xs) { if (f(x)) { ret['@@transducer/step'](x) } } return ret }
filter(x => x % 2 === 1, range(10)) // [ 1, 3, 5, 7, 9 ]
Endlich treffen wir unseren Protagonisten
Überprüfen Sie zunächst noch einmal die vorherige Kartenimplementierung
filter(x => x > 3, new Set(range(10))) // Set (6) {4, 5, 6, 7, 8, 9}
Wir müssen einen Weg finden, die Logik, die vom oben erwähnten Array (Array) abhängt, zu trennen und in einen Reduzierer zu abstrahieren.
range(10) .map(x => x + 1) .filter(x => x % 2 === 1) .slice(0, 3) // [ 1, 3, 5 ]
Die Konstruktion verschwand, die Iteration verschwand und auch die Sammlung von Elementen verschwand.
Durch einen Reduzierer enthält unsere Karte nur die Logik innerhalb ihrer Zuständigkeiten.
Schauen Sie sich den Filter noch einmal an
function compose(...fns) { return fns.reduceRight((acc, fn) => x => fn(acc(x)), x => x) }
Beachten Sie rfilter und den Rückgabetyp von rmap oben:
function curry(f) { return (...args) => data => f(...args, data) }
Es handelt sich eigentlich um einen Transfomer, dessen Parameter und Rückgabewerte Reduzierer sind, also ein Transducer.
Transformer ist zusammensetzbar, also ist auch Transducer zusammensetzbar.
var rmap = curry(map) var rfilter = curry(filter) function take(n, xs) { const ret = new xs.constructor() for (const x of xs) { if (n <= 0) { break } n-- ret['@@transducer/step'](x) } return ret } var rtake = curry(take)
Aber wie benutzt man den Wandler?
take(3, range(10)) // [ 0, 1, 2 ]
take(4, new Set(range(10))) // Set (4) {0, 1, 2, 3}
Wir müssen Iteration und Sammlung mithilfe eines Reduzierers implementieren.
const takeFirst3Odd = compose( rtake(3), rfilter(x => x % 2 === 1), rmap(x => x + 1) ) takeFirst3Odd(range(10)) // [ 1, 3, 5 ]
Es kann jetzt funktionieren und wir haben auch festgestellt, dass die Iteration „on-demand“ erfolgt. Obwohl die Sammlung 100 Elemente enthält, wurden nur die ersten 10 Elemente iteriert.
Als nächstes kapseln wir die obige Logik in eine Funktion.
const map = f => xs => ...
type Transformer = (xs: T) => R
Angenommen, wir haben eine Art asynchrone Datensammlung, beispielsweise einen asynchronen unendlichen Fibonacci-Generator.
data ->> map(...) ->> filter(...) ->> reduce(...) -> result
function pipe(...fns) { return x => fns.reduce((ac, f) => f(ac), x) }
const reduce = (f, init) => xs => xs.reduce(f, init) const f = pipe( rmap(x => x + 1), rfilter(x => x % 2 === 1), rtake(5), reduce((a, b) => a + b, 0) ) f(range(100)) // 25
Wir müssen die Funktion „into“ implementieren, die die oben genannten Datenstrukturen unterstützt.
Posten Sie die Array-Version des Codes als Referenz daneben:
type Transformer = (x: T) => T
Hier ist unser Implementierungscode:
type Reducer = (ac: R, x: T) => R
Der Sammelvorgang ist derselbe, der Iterationsvorgang ist unterschiedlich.
// add is an reducer const add = (a, b) => a + b const sum = xs => xs.reduce(add, 0) sum(range(11)) // 55
Die gleiche Logik gilt für verschiedene Datenstrukturen.
Wenn Sie aufmerksam sind, stellen Sie möglicherweise fest, dass die Parameterreihenfolge der auf „Curry“ basierenden Compose-Version und der auf Reducer basierenden Version unterschiedlich sind.
const list = [1, 2, 3, 4, 5] list.map(x => x + 1) // [ 2, 3, 4, 5, 6 ]
function map(f, xs) { const ret = [] for (let i = 0; i < xs.length; i++) { ret.push(f(xs[i])) } return ret }
Die Ausführung der Funktion erfolgt rechtsassoziativ.
map(x => x + 1, [1, 2, 3, 4, 5]) // [ 2, 3, 4, 5, 6 ]
function filter(f, xs) { const ret = [] for (let i = 0; i < xs.length; i++) { if (f(xs[i])) { ret.push(xs[i]) } } return ret }
Wandler kommen
Wandler – Clojure-Referenz
Das obige ist der detaillierte Inhalt vonWandler: Ein leistungsstarkes Funktionskompositionsmuster. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!