Heim >Web-Frontend >js-Tutorial >Wandler: Ein leistungsstarkes Funktionskompositionsmuster

Wandler: Ein leistungsstarkes Funktionskompositionsmuster

Barbara Streisand
Barbara StreisandOriginal
2025-01-13 14:28:12684Durchsuche

Transducer: A powerful function composition pattern

alias:: Transducer: Ein leistungsstarkes Funktionskompositionsmuster
Notizbuch:: Wandler: 一种强大的函数组合模式

Karte & Filter

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}

komponieren

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 Form der Funktion

Transformator

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()]

Reduzierer

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 ]

Summe

  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
  }

Karte

  Array.prototype['@@transducer/step'] = Array.prototype.push
  // [Function: push]
  map(x => x + 1, [1, 2, 3, 4, 5])
  // [ 2, 3, 4, 5, 6 ]

Filter

  Set.prototype['@@transducer/step'] = Set.prototype.add
  // [Function: add]

nehmen

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 ]

Wandler

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)

in & umwandeln

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

Fließen

Fibonacci-Generator.

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.

Bestellungen

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.

Curry-Version

  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.

Wandlerversion

  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
  }

Referenz

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!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn