誰かがClojureトランスデューサーを簡単に説明してくれませんか?


100

私はこれを読んでみましたが、それでもそれらの価値やそれらが何を置き換えるのかわかりません。そして、それらは私のコードをより短く、より理解しやすいものにしますか?

更新

多くの人が回答を投稿しましたが、トランスデューサーを使用した場合と使用しない場合の例を非常にシンプルなものにして、それを私のようなばかでさえ理解できるとよいでしょう。もちろんトランスデューサーがある程度の高度な理解を必要としない限り、その場合私はそれらを決して理解しません:(

回答:


75

トランスデューサーは、基礎となるシーケンスが何か(それを行う方法)を知らなくても、データシーケンスをどう処理するかを示すレシピです。これは、任意のシーケンス、非同期チャネル、またはおそらく監視可能です。

それらは合成可能で多形です。

利点は、新しいデータソースが追加されるたびにすべての標準コンビネータを実装する必要がないことです。何回も何回も。結果として得られる効果として、ユーザーはこれらのレシピをさまざまなデータソースで再利用できます。

広告更新

Clojureの以前のバージョン1.7では、データフロークエリを作成する方法が3つありました。

  1. ネストされた呼び出し
    (reduce + (filter odd? (map #(+ 2 %) (range 0 10))))
  1. 機能構成
    (def xform
      (comp
        (partial filter odd?)
        (partial map #(+ 2 %))))
    (reduce + (xform (range 0 10)))
  1. スレッド化マクロ
    (defn xform [xs]
      (->> xs
           (map #(+ 2 %))
           (filter odd?)))
    (reduce + (xform (range 0 10)))

トランスデューサーを使用すると、次のように記述できます。

(def xform
  (comp
    (map #(+ 2 %))
    (filter odd?)))
(transduce xform + (range 0 10))

彼らはすべて同じことをします。違いは、トランスデューサを直接呼び出すことはなく、それらを別の関数に渡すことです。トランスデューサは何をすべきかを知っており、トランスデューサを取得する機能はその方法を知っています。コンビネータの順序は、スレッドマクロ(自然順序)を使用して記述する場合と同じです。これxformでチャンネルで再利用できます:

(chan 1 xform)

3
トランスデューサーがどのように時間を節約するかを示す例が付いている答えをもっと探していました。
appshare.co 2014年

Clojureや一部のdataflow libメンテナーでない場合は、そうではありません。
エールRoubíček

5
技術的な決定ではありません。私たちはビジネス価値に基づく決定のみを使用します。「ちょうどそれらを使う」は私を解雇させます
appshare.co

1
Clojure 1.7がリリースされるまでトランスデューサーの使用を遅らせれば、仕事を続けやすくなります。
user100464 2014年

7
トランスデューサーは、さまざまな形式の反復可能なオブジェクトを抽象化する便利な方法のようです。これらは、Clojure seqsなどの非消費型、または(非同期チャネルなど)消費型にすることができます。この点で、たとえば、seqベースの実装からチャネルを使用したcore.async実装に切り替える場合、トランスデューサーを使用することで大きなメリットが得られるように思えます。トランスデューサーを使用すると、ロジックのコアを変更しないでおくことができます。従来のシーケンスベースの処理を使用すると、トランスデューサーまたはコア非同期アナログを使用するようにこれを変換する必要があります。それがビジネスケースです。
Nathan Davis、

47

トランスデューサーは効率を改善し、よりモジュラーな方法で効率的なコードを書くことを可能にします。

これはまともな実行です。

旧への呼び出し構成と比較するとmapfilterreduceあなたが各ステップの間に中間のコレクションを構築し、繰り返し、それらのコレクションを歩く必要はありませんのでなどを、あなたはより良いパフォーマンスを得ます。

と比較しreducersたり、手動ですべての操作を単一の式に構成したりする場合と比べて、抽象化が使いやすくなり、モジュール性が向上し、処理関数を再利用できるようになります。


2
好奇心旺盛なあなたは、「各ステップの間に中間コレクションを構築する」と上で述べました。しかし、「中間コレクション」はアンチパターンのように聞こえませんか?.NETは遅延列挙型を提供し、Javaは遅延ストリームまたはGuava駆動型の反復可能を提供します。遅延型Haskellは遅延型も必要です。これらのいずれも必要としないmap/ reduceそれらのすべてがイテレータチェーンを構築するための中間コレクションを使用します。ここのどこが間違っていますか?
Lyubomyr Shaydariv 2014年

3
ネストするmapfilter、中間コレクションをClojure して作成します。
noisesmith 2014年

4
そして、少なくともClojureのバージョンの遅延については、遅延の問題はここでは直交しています。はい、マップとフィルターは遅延します。また、チェーンすると、遅延値のコンテナーが生成されます。頭を抱えていなければ、不要な大きな遅延シーケンスを構築することはできませんが、遅延要素ごとにこれらの中間抽象化を構築できます。
noisesmith 2014年

一例はいいでしょう。
appshare.co 2014年

8
@LyubomyrShaydariv「中間コレクション」によって、ノイズスミスは「コレクション全体を反復/具体化してから、別のコレクション全体を反復/具体化する」という意味ではありません。彼または彼女は、シーケンシャルを返す関数呼び出しをネストすると、各関数呼び出しの結果、新しいシーケンシャルが作成されることを意味します。実際の反復は依然として1回だけ行われますが、ネストされたシーケンシャルのため、追加のメモリ消費とオブジェクト割り当てがあります。
erikprice 2014年

22

トランスデューサーは、機能を減らすための組み合わせの手段です。

例:縮約関数は、2つの引数を取る関数です。これまでの結果と入力です。彼らは(今のところ)新しい結果を返します。例+:2つの引数を使用すると、最初の引数をこれまでの結果と見なし、2番目の引数を入力と見なすことができます。

トランスデューサーは+関数を取り、2回プラスの関数にすることができます(追加する前にすべての入力を2倍にします)。これは、そのトランスデューサーがどのように見えるかです(最も基本的な用語で):

(defn double
  [rfn]
  (fn [r i] 
    (rfn r (* 2 i))))

どのように2倍以上に変換されるかを確認するためにrfnwithで置き換え+ます+

(def twice-plus ;; result of (double +)
  (fn [r i] 
    (+ r (* 2 i))))

(twice-plus 1 2)  ;-> 5
(= (twice-plus 1 2) ((double +) 1 2)) ;-> true

そう

(reduce (double +) 0 [1 2 3]) 

これで12になります。

トランスデューサから返される還元関数は、知らないうちにどのようにして渡された還元関数とともに蓄積されるため、結果がどのように蓄積されるかには依存しません。ここではのconj代わりに使用します+Conjコレクションと値を取り、その値が追加された新しいコレクションを返します。

(reduce (double conj) [] [1 2 3]) 

[2 4 6]が生成されます

また、入力がどのようなソースであるかにも依存しません。

複数のトランスデューサーを(連鎖可能な)レシピとして連鎖させて、還元機能を変換できます。

更新:現在、それに関する公式ページがあるので、読むことを強くお勧めします:http : //clojure.org/transducers


いい説明ですが、すぐに私には専門用語になりすぎました。「トランスデューサーによって生成される関数の削減は、結果がどのように累積されるかとは無関係です」。
appshare.co 2014年

1
あなたが正しい、生成された単語はここでは不適切でした。
Leon Grapenthin 2014年

大丈夫です。とにかく、トランスフォーマーは現在最適化されているだけなので、おそらく使用しないでください
appshare.co '27 / 10/27

1
それらは機能を減らすための組み合わせの手段です。他にどこにありますか?これは最適化をはるかに超えています。
Leon Grapenthin 2014年

この答えは非常に興味深いと思いますが、それがトランスデューサーにどのように接続するのかはわかりません(主題がまだ混乱しているためです)。関係は何であるdoubleとはtransduce
火星

21

一連の関数を使用してデータのストリームを変換したいとします。Unixシェルでは、パイプ演算子を使用してこのようなことを行うことができます。

cat /etc/passwd | tr '[:lower:]' '[:upper:]' | cut -d: -f1| grep R| wc -l

(上記のコマンドは、ユーザー名に大文字または小文字のいずれかで文字rが付いているユーザーの数をカウントします)。これは一連のプロセスとして実装され、それぞれが前のプロセスの出力から読み取るため、4つの中間ストリームがあります。5つのコマンドを1つの集約コマンドに構成して、入力から読み取り、出力を1回だけ書き込む別の実装を想像できます。中間ストリームが高価であり、構成が安価である場合、それは良いトレードオフになるかもしれません。

同じようなことがClojureにも当てはまります。変換のパイプラインを表現する方法は複数ありますが、その方法によっては、ある関数から次の関数に中間ストリームが渡される場合があります。大量のデータがある場合は、これらの関数を単一の関数に構成する方が高速です。トランスデューサーはそれを行うことを簡単にします。初期のClojureイノベーションであるレデューサーでは、それも可能ですが、いくつかの制限があります。トランスデューサーはそれらの制限のいくつかを取り除きます。

したがって、あなたの質問に答えるために、トランスデューサーは必ずしもコードを短くしたり理解しやすくしたりしませんが、コードもおそらく長くなったり、理解しにくくなったりしません。多くのデータを処理している場合、トランスデューサーはコードを作成できますもっと早く。

これはトランスデューサーのかなり良い概要です。


1
ああ、トランスデューサーはほとんどがパフォーマンスの最適化です、それがあなたが言っていることですか?
appshare.co 2014年

@Zubairはい、そうです。最適化は、中間ストリームを削除するだけではありません。また、操作を並行して実行できる場合もあります。
user100464 2014年

2
言及する価値pmapはありますが、十分な注目を集めていないようです。mapシーケンスを介して高価な関数をpingする場合、操作を並列化するのは「p」を追加するのと同じくらい簡単です。コードの他の部分を変更する必要はありません。現在利用できます。アルファ版ではなく、ベータ版ではありません。(関数が中間シーケンスを作成する場合、トランスデューサーはより高速になると思います。)
Mars

10

リッチヒッキーは、ストレンジループ2014カンファレンス(45分)で「トランスデューサー」の講演を行いました。

彼はトランスデューサーが何であるかを簡単な方法で説明し、実際の例-空港でのバッグの処理。彼は異なる側面を明確に分離し、それらを現在のアプローチと対比しています。最後に向かって、彼は彼らの存在の論理的根拠を与えます。

ビデオ:https : //www.youtube.com/watch?v=6mTbuzafcII


8

私はトランスデューサー-jsの例を読むと、日常のコードでそれらをどのように使用するかという具体的な観点から理解するのに役立ちます。

たとえば、次の例を考えてみてください(上のリンクのREADMEから取得):

var t = require("transducers-js");

var map    = t.map,
    filter = t.filter,
    comp   = t.comp,
    into   = t.into;

var inc    = function(n) { return n + 1; };
var isEven = function(n) { return n % 2 == 0; };
var xf     = comp(map(inc), filter(isEven));

console.log(into([], xf, [0,1,2,3,4])); // [2,4]

1つは、xfUnderscore を使用する通常の方法よりも使用方法がはるかにきれいに見えることです。

_.filter(_.map([0, 1, 2, 3, 4], inc), isEven);

トランスデューサの例はどうしてこんなに長くなるのですか。アンダースコアバージョンはより簡潔に見えます
appshare.co 2014年

1
@Zubairそれほどでもないt.into([], t.comp(t.map(inc), t.filter(isEven)), [0,1,2,3,4])
フアン・カスタニェダ

7

トランスデューサーは(私の理解では)、ある還元関数を取り、別の還元関数を返す関数です。還元機能とは

例えば:

user> (def my-transducer (comp count filter))
#'user/my-transducer
user> (my-transducer even? [0 1 2 3 4 5 6])
4
user> (my-transducer #(< 3 %) [0 1 2 3 4 5 6])
3

この場合、my-transducerは、0に適用される入力フィルタリング関数を使用します。その値が偶数の場合、最初のケースでは、フィルターはその値をカウンターに渡し、次に次の値をフィルターします。最初にフィルタリングしてから、それらの値をすべてカウントに渡す代わりに。

2番目の例でも同じで、一度に1つの値をチェックします。その値が3未満の場合は、countに1を加算します。


私はこの簡単な説明が好きでした
Ignacio

7

トランスデューサの明確な定義はここにあります:

Transducers are a powerful and composable way to build algorithmic transformations that you can reuse in many contexts, and they’re coming to Clojure core and core.async.

それを理解するために、次の簡単な例を考えてみましょう:

;; The Families in the Village

(def village
  [{:home :north :family "smith" :name "sue" :age 37 :sex :f :role :parent}
   {:home :north :family "smith" :name "stan" :age 35 :sex :m :role :parent}
   {:home :north :family "smith" :name "simon" :age 7 :sex :m :role :child}
   {:home :north :family "smith" :name "sadie" :age 5 :sex :f :role :child}

   {:home :south :family "jones" :name "jill" :age 45 :sex :f :role :parent}
   {:home :south :family "jones" :name "jeff" :age 45 :sex :m :role :parent}
   {:home :south :family "jones" :name "jackie" :age 19 :sex :f :role :child}
   {:home :south :family "jones" :name "jason" :age 16 :sex :f :role :child}
   {:home :south :family "jones" :name "june" :age 14 :sex :f :role :child}

   {:home :west :family "brown" :name "billie" :age 55 :sex :f :role :parent}
   {:home :west :family "brown" :name "brian" :age 23 :sex :m :role :child}
   {:home :west :family "brown" :name "bettie" :age 29 :sex :f :role :child}

   {:home :east :family "williams" :name "walter" :age 23 :sex :m :role :parent}
   {:home :east :family "williams" :name "wanda" :age 3 :sex :f :role :child}])

村に何人の子供がいるのか知りたいのですか?次のリデューサーで簡単に見つけることができます:

;; Example 1a - using a reducer to add up all the mapped values

(def ex1a-map-children-to-value-1 (r/map #(if (= :child (:role %)) 1 0)))

(r/reduce + 0 (ex1a-map-children-to-value-1 village))
;;=>
8

これを行う別の方法を次に示します。

;; Example 1b - using a transducer to add up all the mapped values

;; create the transducers using the new arity for map that
;; takes just the function, no collection

(def ex1b-map-children-to-value-1 (map #(if (= :child (:role %)) 1 0)))

;; now use transduce (c.f r/reduce) with the transducer to get the answer 
(transduce ex1b-map-children-to-value-1 + 0 village)
;;=>
8

その上、サブグループも考慮に入れると、それは本当に強力です。たとえば、ブラウンファミリーの子供数を知りたい場合は、次のコマンドを実行できます。

;; Example 2a - using a reducer to count the children in the Brown family

;; create the reducer to select members of the Brown family
(def ex2a-select-brown-family (r/filter #(= "brown" (string/lower-case (:family %)))))

;; compose a composite function to select the Brown family and map children to 1
(def ex2a-count-brown-family-children (comp ex1a-map-children-to-value-1 ex2a-select-brown-family))

;; reduce to add up all the Brown children
(r/reduce + 0 (ex2a-count-brown-family-children village))
;;=>
2

これらの例を参考にしていただければ幸いです。あなたはここでもっと見つけることができます

それが役に立てば幸い。

クレメンシオモラレスルーカス。


3
「トランスデューサーは、多くのコンテキストで再利用できるアルゴリズム変換を構築するための強力で構成可能な方法であり、Clojureコアおよびcore.asyncに登場します。」定義はほとんどすべてに適用できますか?
appshare.co 2014年

1
ほとんどすべてのClojureトランスデューサーに、私は言うでしょう。
クレメンシオモラレスルーカス2014

6
それは定義というよりも、使命の宣言です。
2014年

4

私はこれについてclojurescriptの例を使ってブログに書いたこれについてを、還元関数を置き換えることができるようになり、シーケンス関数がどのように拡張できるようになるかを説明しています。

これが私が読んだトランスデューサーのポイントです。consまたはのconjような操作にハードコーディングされている操作について考えるとmapfilterなど、軽減機能が到達不能でした。

トランスデューサーを使用すると、還元機能が分離され、pushトランスデューサーのおかげでネイティブのJavaScript配列と同じようにそれを置き換えることができます。

(transduce (filter #(not (.hasOwnProperty prevChildMapping %))) (.-push #js[]) #js [] nextKeys)

filter 友人たちは、独自の還元関数を提供するために使用できる変換関数を返す新しい1アリティ演算を持っています。


4

ここに私の(ほとんど)専門用語とコードフリーの答えがあります。

データは、ストリーム(イベントなどの時間の経過とともに発生する値)または構造(リスト、ベクトル、配列など、ある時点で存在するデータ)の2つの方法で考えます。

ストリームまたは構造のいずれかに対して実行する必要がある特定の操作があります。そのような操作の1つがマッピングです。マッピング関数は、各データ項目(数値であると想定)を1ずつ増やす可能性があり、これがストリームまたは構造体にどのように適用されるか想像できます。

マッピング関数は、「還元関数」と呼ばれることもある関数のクラスの1つにすぎません。もう1つの一般的な削減関数は、述語に一致する値を削除するフィルターです(偶数のすべての値を削除するなど)。

トランスデューサーを使用すると、1つ以上のレデューシング関数のシーケンスを「ラップ」して、両方のストリームまたは構造で機能する「パッケージ」(それ自体が関数)を生成できます。たとえば、一連のリダクション関数を「パッケージ化」し(偶数をフィルタリングし、結果の数値をマップして1ずつ増やす)、ストリームまたは値の構造(またはその両方)でそのトランスデューサー「パッケージ」を使用できます。 。

これは何が特別なのでしょうか?通常、削減関数は、ストリームと構造の両方で機能するように効率的に構成することができません。

したがって、これらの機能に関する知識を活用して、より多くのユースケースに適用できるというメリットがあります。あなたにとってのコストは、あなたにこの追加の力を与えるためにいくつかの追加の機械(すなわちトランスデューサー)を学ばなければならないということです。


2

私が理解している限り、それらはビルディングブロックのようなもので、入力と出力の実装から切り離されています。操作を定義するだけです。

操作の実装は入力のコードにはなく、出力では何も行われないため、トランスデューサは非常に再利用可能です。彼らはを思い出させるフローでのアッカストリーム

私はトランスデューサーも初めてですが、答えが不明確な可能性があります。


1

この投稿では、トランスデューサーの鳥瞰図を紹介しています。

https://medium.com/@roman01la/understanding-transducers-in-javascript-3500d3bd9624


3
外部リンクのみに依存する回答は、将来いつでもリンクが壊れる可能性があるため、SOでは推奨されません。代わりに、回答の内容を引用してください。
Vincent Cantin 2018年

@VincentCantin実際、Mediumの投稿は削除されました。
Dmitri Zaitsev

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.