ホームページ >ウェブフロントエンド >jsチュートリアル >トランスデューサー:強力な機能構成パターン

トランスデューサー:強力な機能構成パターン

Barbara Streisand
Barbara Streisandオリジナル
2025-01-13 14:28:12684ブラウズ

Transducer: A powerful function composition pattern

alias:: トランスデューサー: 強力な関数構成パターン
ノートブック:: トランスデューサー: 一种强大的関数数集合モード

マップとフィルター

map のセマンティクスは「マッピング」です。これは、セット内のすべての要素に対して変換を 1 回実行することを意味します。

  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 ]

上記では、マップの実装がコレクション型に依存していることを明確に表現するために、意図的に for ステートメントを使用しています。
順次実行;
怠惰ではなく即時評価。
フィルタを見てみましょう:

  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 ]

同様に、フィルターの実装も特定のコレクション タイプに依存し、現在の実装では xs が配列である必要があります。
マップはどのようにしてさまざまなデータ型をサポートできるのでしょうか?たとえば、Set、Map、カスタム データ タイプなどです。
従来の方法があります。コレクションのインターフェイス (プロトコル) に依存します。
言語が異なれば実装も異なります。JS のネイティブ サポートはこの点では比較的弱いですが、これも可能です。
Symbol.iterator を使用して反復処理します。
Object#constractor を使用してコンストラクターを取得します。
では、プッシュでさまざまなデータ型を抽象的にサポートするにはどうすればよいでしょうか?
ramdajs ライブラリを模倣し、カスタム @@transducer/step 関数に依存できます。

  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}

この方法を使用すると、マップ、フィルターなど、より軸的な機能を実装できます。
重要なのは、構築、反復、コレクションなどの操作を特定のコレクション クラスに委任することです。これらの操作を完了する方法を知っているのはコレクション自体だけであるためです。

  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}

作曲する

上記のマップとフィルタを組み合わせて使用​​すると、いくつかの問題が発生します。

  range(10)
    .map(x => x + 1)
    .filter(x => x % 2 === 1)
    .slice(0, 3)
  // [ 1, 3, 5 ]

使用される要素は 5 つだけですが、コレクション内のすべての要素が走査されます。
各ステップで中間コレクション オブジェクトが生成されます。
Compose を使用してこのロジックを再度実装します

  function compose(...fns) {
    return fns.reduceRight((acc, fn) => x => fn(acc(x)), x => x)
  }

合成をサポートするために、マップやフィルターなどの関数をカレーの形式で実装します。

  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 ]

これまでのところ、私たちの実装は表現的には明確かつ簡潔ですが、実行時には無駄です。

関数の形状

トランス

バージョンカレーのマップ関数は次のようになります:

  const map = f => xs => ...

つまり、map(x => ...) は単一パラメータ関数を返します。

  const list = [1, 2, 3, 4, 5]

  list.map(x => x + 1)
  // [ 2, 3, 4, 5, 6 ]

パラメータが 1 つの関数は簡単に作成できます。
具体的には、これらの関数の入力は「データ」、出力は処理されたデータであり、関数はデータ変換器 (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 は単一パラメータ関数であり、関数の合成に便利です。

  var range = n => [...Array(n).keys()]

減速機

リデューサーは、より複雑なロジックを表現するために使用できる 2 つのパラメーターの関数です。

  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]

取る

take を実装するにはどうすればよいですか?これには、reduce に Break と同様の機能を持たせる必要があります。

  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 ]

トランスデューサー

ついに主人公に会います
まず、以前のマップ実装を再検討します

  filter(x => x > 3, new Set(range(10)))
  // Set (6) {4, 5, 6, 7, 8, 9}

上記の配列 (Array) に依存するロジックを分離し、Reducer に抽象化する方法を見つける必要があります。

  range(10)
    .map(x => x + 1)
    .filter(x => x % 2 === 1)
    .slice(0, 3)
  // [ 1, 3, 5 ]

構築は消滅し、反復は消滅し、要素のコレクションも消滅しました。
Reducer を介して、マップにはその責任範囲内のロジックのみが含まれます。
フィルタをもう一度見てください

  function compose(...fns) {
    return fns.reduceRight((acc, fn) => x => fn(acc(x)), x => x)
  }

上記の rfilter と rmap の戻り値の型に注意してください:

  function curry(f) {
    return (...args) => data => f(...args, data)
  }

これは実際には Transformer であり、パラメータと戻り値の両方が Reducer であるため、 Transducer です。
Transformer はコンポーザブルであるため、Transducer もコンポーザブルです。

  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 ]

これで動作するようになり、反復が「オンデマンド」であることにも気付きました。コレクションには 100 個の要素がありますが、最初の 10 個の要素のみが反復されました。
次に、上記のロジックを関数にカプセル化します。

  const map = f => xs => ...
  type Transformer = (xs: T) => R

流れ

フィボナッチジェネレータ。

非同期無限フィボナッチジェネレータなど、ある種の非同期データ収集があると仮定します。

  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

上記のデータ構造をサポートする関数を実装する必要があります。
参考としてコードの配列バージョンをその隣に投稿します:

  type Transformer = (x: T) => T

実装コードは次のとおりです:

  type Reducer = (ac: R, x: T) => R

コレクション操作は同じですが、反復操作は異なります。

  // add is an reducer
  const add = (a, b) => a + b
  const sum = xs => xs.reduce(add, 0)

  sum(range(11))
  // 55

同じロジックが異なるデータ構造に適用されます。

注文

注意深い方は、curry に基づいた compose バージョンと、reducer に基づいたバージョンのパラメータの順序が異なることに気づくかもしれません。

カレーバージョン

  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 ]
  function filter(f, xs) {
    const ret = []
    for (let i = 0; i < xs.length; i++) {
      if (f(xs[i])) {
        ret.push(xs[i])
      }
    }
    return ret
  }

参照

トランスデューサーが登場します
トランスデューサ - Clojure リファレンス

以上がトランスデューサー:強力な機能構成パターンの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。