Clojure

トランスデューサ

トランスデューサは、合成可能なアルゴリズム変換です。入力と出力ソースのコンテキストから独立しており、個々の要素に関する変換の本質のみを指定します。トランスデューサは入力または出力ソースから切り離されているため、コレクション、ストリーム、チャネル、オブザーバブルなど、多くの異なるプロセスで使用できます。トランスデューサは、入力への認識や中間集計の作成なしに直接合成されます。

導入的なブログ記事、このビデオ、およびFAQのこのセクショントランスデューサの良いユースケースについても参照してください。

用語

リダクション関数とは、**reduce**に渡すような関数のことです。これは、蓄積された結果と新しい入力を受け取り、新しい蓄積された結果を返す関数です。

;; reducing function signature
whatever, input -> whatever

トランスデューサ(xformまたはxfとも呼ばれる)は、あるリダクション関数から別のリダクション関数への変換です。

;; transducer signature
(whatever, input -> whatever) -> (whatever, input -> whatever)

トランスデューサによる変換の定義

Clojureに含まれるほとんどのシーケンス関数は、トランスデューサを生成するアリティを持っています。このアリティは入力コレクションを省略します。入力は、トランスデューサを適用するプロセスによって供給されます。注:この縮小されたアリティは、カリー化または部分適用ではありません。

例えば

(filter odd?) ;; returns a transducer that filters odd
(map inc)     ;; returns a mapping transducer for incrementing
(take 5)      ;; returns a transducer that will take the first 5 values

トランスデューサは、通常の関数合成で合成されます。トランスデューサは、ラップするトランスデューサを呼び出すかどうか、何回呼び出すかを決定する前に、その操作を実行します。トランスデューサを合成する推奨方法は、既存の**comp**関数を使用することです。

(def xf
  (comp
    (filter odd?)
    (map inc)
    (take 5)))

トランスデューサxfは、一連の入力要素にプロセスによって適用される変換スタックです。スタック内の各関数は、それがラップする操作の前に実行されます。トランスフォーマの合成は右から左に実行されますが、左から右に実行される変換スタックを構築します(この例では、フィルタリングはマッピングの前に実行されます)。

ニーモニックとして、**comp**でのトランスデューサ関数の順序は、**->>**でのシーケンス変換の順序と同じであることを覚えておいてください。上記の変換は、シーケンス変換と同等です。

(->> coll
     (filter odd?)
     (map inc)
     (take 5))

次の関数は、入力コレクションが省略されるとトランスデューサを生成します:map cat mapcat filter remove take take-while take-nth drop drop-while replace partition-by partition-all keep keep-indexed map-indexed distinct interpose dedupe random-sample

トランスデューサの使用

トランスデューサは多くのコンテキストで使用できます(新しいトランスデューサの作成方法については以下を参照してください)。

transduce

トランスデューサを適用する最も一般的な方法の1つは、transduce関数を使用することです。これは標準的なreduce関数と類似しています。

(transduce xform f coll)
(transduce xform f init coll)

**transduce**は、トランスデューサ**xform**をリダクション関数**f**に適用して、**coll**を遅延せずにすぐにリダクションします。初期値としてinitが指定されている場合はそれを、そうでない場合は(f)を使用します。fは結果を蓄積する方法に関する知識を提供し、それはreduceの(状態を持つ可能性のある)コンテキストで発生します。

(def xf (comp (filter odd?) (map inc)))
(transduce xf + (range 5))
;; => 6
(transduce xf + 100 (range 5))
;; => 106

合成されたxfトランスデューサは、リダクション関数fへの最終的な呼び出しとともに、左から右に呼び出されます。最後の例では、入力値は最初にフィルタリングされ、次にインクリメントされ、最後に合計されます。

Nested transformations

eduction

トランスデューサをcollに適用するプロセスをキャプチャするには、eduction関数を使用します。これは、任意の数のxformと最終的なcollを受け取り、coll内のアイテムへのトランスデューサの還元可能な/反復可能なアプリケーションを返します。これらのアプリケーションは、reduce/iteratorが呼び出されるたびに実行されます。

(def iter (eduction xf (range 5)))
(reduce + 0 iter)
;; => 6

into

トランスデューサを入力コレクションに適用して新しい出力コレクションを作成するには、intoを使用します(可能であれば、reduceとトランジェントを効率的に使用します)。

(into [] xf (range 1000))

sequence

トランスデューサを入力コレクションに適用してシーケンスを作成するには、sequenceを使用します。

(sequence xf (range 1000))

結果のシーケンス要素は、増分的に計算されます。これらのシーケンスは必要に応じて入力が増分的に消費され、中間操作が完全に実現されます。この動作は、遅延シーケンスに対する同等の操作とは異なります。

トランスデューサの作成

トランスデューサは次の形状をしています(「…​」はカスタムコードです)。

(fn [rf]
  (fn ([] ...)
      ([result] ...)
      ([result input] ...)))

コアシーケンス関数(map、filterなど)の多くは、操作固有の引数(述語、関数、カウントなど)を受け取り、これらの引数をクローズするこの形状のトランスデューサを返します。**cat**のように、コア関数がトランスデューサ関数であり、**rf**を取らない場合もあります。

内部関数は、異なる目的で使用される3つのアリティで定義されています。

  • **Init**(アリティ0)-ネストされた変換**rf**のinitアリティを呼び出す必要があります。これは最終的にトランスデューシングプロセスを呼び出します。

  • **Step**(アリティ2)-これは標準的なリダクション関数ですが、トランスデューサでは必要に応じて**rf**ステップアリティを0回以上呼び出すことが期待されます。たとえば、filterは(述語に基づいて)**rf**を呼び出すかどうかを選択します。mapは常に正確に1回呼び出します。catは入力に応じて複数回呼び出す可能性があります。

  • **Completion**(アリティ1)-一部のプロセスは終了しませんが、**transduce**のように終了するプロセスでは、completionアリティを使用して最終値を生成したり、状態をフラッシュしたりするために使用されます。このアリティは、**rf** completionアリティを正確に1回呼び出す必要があります。

**completion**の使用例として、入力の最後に残りの要素をフラッシュする必要がある**partition-all**があります。completing関数は、デフォルトのcompletionアリティを追加することで、リダクション関数をトランスデューシング関数に変換するために使用できます。

早期終了

Clojureには、reduceの早期終了を指定するメカニズムがあります。

  • reduced - 値を受け取り、リダクションを停止する必要があることを示すreduced値を返します。

  • reduced? - 値がreducedで作成された場合はtrueを返します。

  • derefまたは@を使用して、reduced内の値を取得できます。

トランスデューサを使用するプロセスは、ステップ関数がreduced値を返したときに確認して停止する必要があります(トランスデューサブルプロセスの作成で詳しく説明します)。さらに、ネストされたreduceを使用するトランスデューサステップ関数は、reduced値が発生したときに確認して伝達する必要があります。(例としてcatの実装を参照してください)。

リダクション状態を持つトランスデューサ

一部のトランスデューサ(**take**、**partition-all**など)は、リダクションプロセス中に状態が必要です。この状態は、トランスデューサブルプロセスがトランスデューサを適用するたびに作成されます。たとえば、一連の重複した値を単一の値に折りたたむdedupeトランスデューサを考えてみましょう。このトランスデューサは、現在の値を渡す必要があるかどうかを判断するために、前の値を覚えておく必要があります。

(defn dedupe []
  (fn [xf]
    (let [prev (volatile! ::none)]
      (fn
        ([] (xf))
        ([result] (xf result))
        ([result input]
          (let [prior @prev]
            (vreset! prev input)
              (if (= prior input)
                result
                (xf result input))))))))

dedupeにおいて、**prev**は、リダクション中に以前の値を格納する状態を持つコンテナです。 パフォーマンスのため、prev値はvolatile型ですが、atom型でも構いません。 prev値は、トランスデューシングプロセスが開始されるまで(例えば**transduce**への呼び出し時まで)初期化されません。 したがって、状態を持つ相互作用は、トランスデューサブルプロセスのコンテキスト内に含まれます。

完了ステップでは、リダクション状態を持つトランスデューサは、ネストされたトランスフォーマの完了関数を呼び出す前に状態をフラッシュする必要があります。ただし、ネストされたステップから既にリダクションされた値が見られている場合は、保留中の状態は破棄する必要があります。

トランスデューサブルプロセスの作成

トランスデューサは、多くの種類のプロセスで使用されるように設計されています。 トランスデューサブルプロセスは、各ステップが入力を摂取する一連のステップとして定義されます。 入力ソースはプロセスごとに固有です(コレクション、イテレータ、ストリームなど)。 同様に、プロセスは各ステップによって生成された出力に対してどのような処理を行うかを選択する必要があります。

トランスデューサを適用するための新しいコンテキストがある場合、考慮すべきいくつかの一般的なルールがあります。

  • ステップ関数がreduced値を返す場合、トランスデューサブルプロセスはステップ関数にそれ以上の入力を供給してはなりません。 reduced値は、完了前にderefを使用してアンラップする必要があります。

  • 完了プロセスは、最終的な累積値に対して完了操作を正確に一度呼び出す必要があります。

  • トランスデューシングプロセスは、トランスデューサを呼び出すことで返される関数への参照をカプセル化する必要があります。これらは状態を持つ可能性があり、スレッド間で使用するには安全ではありません。