Kann mir jemand Clojure Transducers in einfachen Worten erklären?


100

Ich habe versucht, dies nachzulesen, aber ich verstehe immer noch nicht, welchen Wert sie haben oder was sie ersetzen. Und machen sie meinen Code kürzer, verständlicher oder was?

Aktualisieren

Viele Leute haben Antworten gepostet, aber es wäre schön, Beispiele mit und ohne Wandler für etwas sehr Einfaches zu sehen, das selbst ein Idiot wie ich verstehen kann. Es sei denn natürlich, Wandler benötigen ein gewisses Maß an Verständnis. In diesem Fall werde ich sie nie verstehen :(

Antworten:


75

Wandler sind Rezepte, was mit einer Datensequenz zu tun ist, ohne zu wissen, welche Sequenz zugrunde liegt (wie es geht). Es kann sich um eine beliebige Sequenz, einen asynchronen Kanal oder einen beobachtbaren Kanal handeln.

Sie sind zusammensetzbar und polymorph.

Der Vorteil ist, dass Sie nicht jedes Mal, wenn eine neue Datenquelle hinzugefügt wird, alle Standardkombinatoren implementieren müssen. Wieder und wieder. Als Ergebnis können Sie als Benutzer diese Rezepte in verschiedenen Datenquellen wiederverwenden.

Anzeigenaktualisierung

In der vorherigen Version 1.7 von Clojure hatten Sie drei Möglichkeiten, Datenflussabfragen zu schreiben:

  1. verschachtelte Aufrufe
    (reduce + (filter odd? (map #(+ 2 %) (range 0 10))))
  1. funktionelle Zusammensetzung
    (def xform
      (comp
        (partial filter odd?)
        (partial map #(+ 2 %))))
    (reduce + (xform (range 0 10)))
  1. Threading-Makro
    (defn xform [xs]
      (->> xs
           (map #(+ 2 %))
           (filter odd?)))
    (reduce + (xform (range 0 10)))

Mit Wandlern schreiben Sie es wie folgt:

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

Sie alle machen das Gleiche. Der Unterschied besteht darin, dass Sie Transducer niemals direkt aufrufen, sondern an eine andere Funktion übergeben. Wandler wissen, was zu tun ist, die Funktion, die Wandler erhält, weiß wie. Die Reihenfolge der Kombinatoren ist wie beim Schreiben mit einem Threading-Makro (natürliche Reihenfolge). Jetzt können Sie xformmit Kanal wiederverwenden :

(chan 1 xform)

3
Ich suchte eher nach einer Antwort, die ein Beispiel enthält, das mir zeigt, wie Wandler mir Zeit sparen.
appshare.co

Sie tun dies nicht, wenn Sie nicht Clojure oder ein Datenflussbibliotheksbetreuer sind.
Aleš Roubíček

5
Es ist keine technische Entscheidung. Wir verwenden nur geschäftswertbasierte Entscheidungen. "
Benutze

1
Möglicherweise fällt es Ihnen leichter, Ihren Job zu behalten, wenn Sie den Versuch, Wandler zu verwenden, verzögern, bis Clojure 1.7 veröffentlicht wird.
user100464

7
Wandler scheinen eine nützliche Möglichkeit zu sein, über verschiedene Formen iterierbarer Objekte zu abstrahieren. Diese können nicht konsumierbar sein, wie z. B. Clojure-Sequenzen, oder konsumierbar (wie z. B. asynchrone Kanäle). In dieser Hinsicht scheint es mir, dass Sie von der Verwendung von Wandlern stark profitieren würden, wenn Sie beispielsweise von einer seq-basierten Implementierung zu einer core.async-Implementierung über Kanäle wechseln würden. Wandler sollten es Ihnen ermöglichen, den Kern Ihrer Logik unverändert zu lassen. Bei Verwendung der herkömmlichen sequenzbasierten Verarbeitung müssten Sie diese konvertieren, um entweder Wandler oder ein Core-Async-Analog zu verwenden. Das ist der Business Case.
Nathan Davis

47

Wandler verbessern die Effizienz und ermöglichen es Ihnen, effizienten Code modularer zu schreiben.

Dies ist ein anständiger Durchlauf .

Im Vergleich zu Anrufen zu dem alten Komponieren map, filter, reduceusw. erhalten Sie eine bessere Leistung , weil Sie nicht brauchen , um zwischen den einzelnen Schritten Zwischen Sammlungen zu bauen, und wiederholt diese Sammlungen zu gehen.

Im Vergleich zu reducersoder dem manuellen Zusammenstellen aller Vorgänge in einem einzigen Ausdruck können Sie Abstraktionen einfacher verwenden, die Modularität verbessern und die Verarbeitungsfunktionen wiederverwenden.


2
Nur neugierig, sagten Sie oben: "Zwischen Sammlungen zwischen jedem Schritt zu bauen". Aber klingt "Zwischensammlungen" nicht wie ein Anti-Pattern? .NET bietet Lazy Enumerables, Java Lazy Streams oder Guava-gesteuerte Iterables, Lazy Haskell muss auch etwas Lazy haben. Keine dieser Anwendungen erfordert map/ muss reduceZwischensammlungen verwenden, da alle eine Iteratorkette bilden. Wo irre ich mich hier?
Lyubomyr Shaydariv

3
Clojure mapund filtererstelle Zwischensammlungen, wenn sie verschachtelt sind.
Geräuschschmied

4
Und zumindest in Bezug auf Clojures Version der Faulheit ist das Thema Faulheit hier orthogonal. Ja, Map und Filter sind faul, sie generieren auch Container für faule Werte, wenn Sie sie verketten. Wenn Sie sich nicht am Kopf festhalten, bauen Sie keine großen faulen Sequenzen auf, die nicht benötigt werden, aber Sie bauen trotzdem diese Zwischenabstraktionen für jedes faule Element.
Geräuschschmied

Ein Beispiel wäre schön.
appshare.co

8
@LyubomyrShaydariv Mit "Zwischensammlung" meint Noisesmith nicht "eine ganze Sammlung iterieren / reifizieren, dann eine andere ganze Sammlung iterieren / reifizieren". Er oder sie bedeutet, dass beim Verschachteln von Funktionsaufrufen, die Sequentiale zurückgeben, jeder Funktionsaufruf zur Erstellung einer neuen Sequentialfunktion führt. Die eigentliche Iteration findet immer noch nur einmal statt, aber aufgrund der verschachtelten Sequenzen kommt es zu einem zusätzlichen Speicherverbrauch und einer zusätzlichen Objektzuweisung.
Erikprice

22

Wandler sind ein Kombinationsmittel zur Reduzierung von Funktionen.

Beispiel: Reduzierungsfunktionen sind Funktionen, die zwei Argumente annehmen: Ein bisheriges Ergebnis und eine Eingabe. Sie geben (bisher) ein neues Ergebnis zurück. Beispiel +: Mit zwei Argumenten können Sie sich das erste als Ergebnis und das zweite als Eingabe vorstellen.

Ein Wandler könnte nun die + -Funktion übernehmen und daraus eine Doppel-Plus-Funktion machen (verdoppelt jeden Eingang, bevor er hinzugefügt wird). So würde dieser Wandler aussehen (in den grundlegendsten Begriffen):

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

Zur Veranschaulichung ersetzen Sie rfndurch, um +zu sehen, wie +sich in ein Plus-Doppel verwandelt:

(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

So

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

würde jetzt 12 ergeben.

Von Wandlern zurückgegebene Reduzierungsfunktionen sind unabhängig davon, wie das Ergebnis akkumuliert wird, da sie sich mit der an sie übergebenen Reduzierungsfunktion akkumulieren, ohne es zu wissen. Hier verwenden wir conjstatt +. ConjNimmt eine Sammlung und einen Wert und gibt eine neue Sammlung mit diesem angehängten Wert zurück.

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

würde ergeben [2 4 6]

Sie sind auch unabhängig davon, um welche Art von Quelle es sich handelt.

Mehrere Wandler können als (verkettbares) Rezept verkettet werden, um reduzierende Funktionen zu transformieren.

Update: Da es jetzt eine offizielle Seite darüber gibt, empfehle ich dringend, sie zu lesen: http://clojure.org/transducers


Schöne Erklärung, aber bald zu viel Fachjargon für mich: "Reduzierende Funktionen, die von Wandlern erzeugt werden, sind unabhängig davon, wie das Ergebnis akkumuliert wird."
appshare.co

1
Sie haben Recht, das generierte Wort war hier unangemessen.
Leon Grapenthin

Es ist in Ordnung. Wie auch immer, ich verstehe, dass Transformatoren jetzt nur eine Optimierung sind und daher wahrscheinlich sowieso nicht verwendet werden sollten
appshare.co

1
Sie sind ein Kombinationsmittel zur Reduzierung von Funktionen. Wo sonst hast du das? Dies ist weit mehr als eine Optimierung.
Leon Grapenthin

Ich finde diese Antwort sehr interessant, aber mir ist nicht klar, wie sie mit Wandlern verbunden ist (teilweise, weil ich das Thema immer noch verwirrend finde). Wie ist die Beziehung zwischen doubleund transduce?
Mars

21

Angenommen, Sie möchten eine Reihe von Funktionen verwenden, um einen Datenstrom zu transformieren. Mit der Unix-Shell können Sie so etwas mit dem Pipe-Operator tun, z

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

(Der obige Befehl zählt die Anzahl der Benutzer, deren Benutzername den Buchstaben r in Groß- oder Kleinbuchstaben enthält.) Dies wird als eine Reihe von Prozessen implementiert, von denen jeder aus der Ausgabe der vorherigen Prozesse liest, sodass vier Zwischenströme vorhanden sind. Sie können sich eine andere Implementierung vorstellen, die die fünf Befehle zu einem einzigen Aggregatbefehl zusammensetzt, der genau einmal aus seiner Eingabe liest und seine Ausgabe schreibt. Wenn Zwischenströme teuer und die Zusammensetzung billig wären, könnte dies ein guter Kompromiss sein.

Das Gleiche gilt für Clojure. Es gibt mehrere Möglichkeiten, eine Pipeline von Transformationen auszudrücken. Je nachdem, wie Sie dies tun, können Zwischenströme von einer Funktion zur nächsten übertragen werden. Wenn Sie viele Daten haben, ist es schneller, diese Funktionen zu einer einzigen Funktion zusammenzufassen. Wandler machen das einfach. Eine frühere Clojure-Innovation, Reduzierungen, lässt Sie dies ebenfalls tun, jedoch mit einigen Einschränkungen. Wandler beseitigen einige dieser Einschränkungen.

Um Ihre Frage zu beantworten, machen Wandler Ihren Code nicht unbedingt kürzer oder verständlicher, aber Ihr Code wird wahrscheinlich auch nicht länger oder weniger verständlich sein, und wenn Sie mit vielen Daten arbeiten, können Wandler Ihren Code machen schneller.

Dies ist eine ziemlich gute Übersicht über Wandler.


1
Ah, Wandler sind also meistens eine Leistungsoptimierung. Ist es das, was Sie sagen?
appshare.co

@Zubair Ja, das stimmt. Beachten Sie, dass die Optimierung über das Eliminieren von Zwischenströmen hinausgeht. Möglicherweise können Sie auch Operationen parallel ausführen.
user100464

2
Es ist erwähnenswert pmap, was nicht genug Aufmerksamkeit zu bekommen scheint. Wenn Sie mapeine teure Funktion über eine Sequenz pingen, ist die parallele Operation so einfach wie das Hinzufügen von "p". Sie müssen nichts anderes in Ihrem Code ändern und es ist jetzt verfügbar - nicht Alpha, nicht Beta. (Wenn die Funktion Zwischensequenzen erstellt, sind die Wandler möglicherweise schneller, denke ich.)
Mars

10

Rich Hickey hielt auf der Strange Loop 2014-Konferenz (45 Minuten) einen Vortrag über „Transducers“.

Er erklärt auf einfache Weise, was Wandler sind, anhand von Beispielen aus der Praxis - der Verarbeitung von Taschen auf einem Flughafen. Er trennt die verschiedenen Aspekte klar voneinander und kontrastiert sie mit den aktuellen Ansätzen. Gegen Ende gibt er die Gründe für ihre Existenz an.

Video: https://www.youtube.com/watch?v=6mTbuzafcII


8

Ich habe festgestellt, dass das Lesen von Beispielen von Transducers-js mir hilft, sie konkret zu verstehen, wie ich sie im täglichen Code verwenden könnte.

Betrachten Sie zum Beispiel dieses Beispiel (entnommen aus der README-Datei unter dem obigen Link):

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]

Zum einen xfsieht die Verwendung viel sauberer aus als die übliche Alternative mit Underscore.

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

Wie kommt es, dass das Beispiel für Wandler so viel länger ist? Die Unterstrich-Version sieht viel prägnanter aus
appshare.co

1
@ Kubair Nicht wirklicht.into([], t.comp(t.map(inc), t.filter(isEven)), [0,1,2,3,4])
Juan Castañeda

7

Wandler sind (nach meinem Verständnis!) Funktionen, die eine Reduktionsfunktion übernehmen und eine andere zurückgeben. Eine reduzierende Funktion ist eine, die

Beispielsweise:

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

In diesem Fall übernimmt my-transducer eine Eingangsfilterfunktion, die auf 0 angewendet wird. Wenn dieser Wert gerade ist? Im ersten Fall übergibt der Filter diesen Wert an den Zähler und filtert dann den nächsten Wert. Anstatt zuerst zu filtern und dann alle diese Werte zu übergeben, um zu zählen.

Es ist dasselbe wie im zweiten Beispiel, in dem jeweils ein Wert überprüft wird. Wenn dieser Wert kleiner als 3 ist, können Sie 1 addieren.


Ich mochte diese einfache Erklärung
Ignacio

7

Eine klare Definition des Wandlers finden Sie hier:

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.

Um es zu verstehen, betrachten wir das folgende einfache Beispiel:

;; 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}])

Was ist damit, wir wollen wissen, wie viele Kinder im Dorf sind? Wir können es leicht mit dem folgenden Reduzierer herausfinden:

;; 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

Hier ist eine andere Möglichkeit:

;; 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

Außerdem ist es sehr leistungsfähig, wenn auch Untergruppen berücksichtigt werden. Wenn wir zum Beispiel wissen möchten, wie viele Kinder in der Familie Brown sind, können wir Folgendes ausführen:

;; 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

Ich hoffe, Sie finden diese Beispiele hilfreich. Mehr finden Sie hier

Ich hoffe es hilft.

Clemencio Morales Lucas.


3
Die "Transducer sind eine leistungsstarke und zusammensetzbare Methode zum Erstellen algorithmischer Transformationen, die Sie in vielen Kontexten wiederverwenden können, und sie kommen zu Clojure core und core.async." Definition könnte für fast alles gelten?
appshare.co

1
Zu fast jedem Clojure Transducer würde ich sagen.
Clemencio Morales Lucas

6
Es ist eher ein Leitbild als eine Definition.
Mars

4

Ich habe darüber mit einem Clojurescript- Beispiel gebloggt, das erklärt, wie die Sequenzfunktionen jetzt erweiterbar sind, indem die Reduktionsfunktion ersetzt werden kann.

Dies ist der Punkt der Wandler, wie ich es lese. Wenn Sie an die consoder -Operation denken, die conjin Operationen wie usw. fest codiert ist map, filterwar die Reduzierungsfunktion nicht erreichbar.

Bei Wandlern ist die Reduktionsfunktion entkoppelt und ich kann sie pushdank Wandlern wie beim nativen Javascript-Array ersetzen .

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

filter und Freunde haben eine neue 1-Arity-Operation, die eine Wandlerfunktion zurückgibt, mit der Sie Ihre eigene Reduktionsfunktion bereitstellen können.


4

Hier ist meine (meistens) jargon- und codefreie Antwort.

Stellen Sie sich Daten auf zwei Arten vor: als Stream (Werte, die im Laufe der Zeit auftreten, z. B. Ereignisse) oder als Struktur (Daten, die zu einem bestimmten Zeitpunkt vorhanden sind, z. B. eine Liste, ein Vektor, ein Array usw.).

Es gibt bestimmte Vorgänge, die Sie möglicherweise über Streams oder Strukturen ausführen möchten. Eine solche Operation ist das Mapping. Eine Zuordnungsfunktion kann jedes Datenelement (vorausgesetzt, es ist eine Zahl) um 1 erhöhen, und Sie können sich hoffentlich vorstellen, wie dies entweder auf einen Stream oder eine Struktur angewendet werden könnte.

Eine Zuordnungsfunktion ist nur eine aus einer Klasse von Funktionen, die manchmal als "Reduktionsfunktionen" bezeichnet werden. Eine weitere häufig verwendete Reduktionsfunktion ist der Filter, mit dem Werte entfernt werden, die mit einem Prädikat übereinstimmen (z. B. alle geraden Werte entfernen).

Mit Wandlern können Sie eine Folge von einer oder mehreren reduzierenden Funktionen "umschließen" und ein "Paket" (das selbst eine Funktion ist) erstellen, das sowohl für Streams als auch für Strukturen funktioniert. Zum Beispiel könnten Sie eine Folge von Reduzierungsfunktionen "verpacken" (z. B. gerade Zahlen filtern und dann die resultierenden Zahlen zuordnen, um sie um 1 zu erhöhen) und dann dieses "Paket" des Wandlers entweder für einen Stream oder eine Wertestruktur (oder für beide) verwenden. .

Was ist das Besondere daran? In der Regel können reduzierende Funktionen nicht effizient zusammengesetzt werden, um sowohl an Streams als auch an Strukturen zu arbeiten.

Der Vorteil für Sie besteht also darin, dass Sie Ihr Wissen über diese Funktionen nutzen und auf weitere Anwendungsfälle anwenden können. Die Kosten für Sie sind, dass Sie einige zusätzliche Maschinen (dh den Wandler) erlernen müssen, um diese zusätzliche Leistung zu erhalten.


2

Soweit ich weiß, sind sie wie Bausteine , die von der Eingabe- und Ausgabeimplementierung entkoppelt sind. Sie definieren nur die Operation.

Da die Implementierung der Operation nicht im Code der Eingabe enthalten ist und mit der Ausgabe nichts unternommen wird, sind Wandler äußerst wiederverwendbar. Sie erinnern mich an Flow s in Akka Streams .

Ich bin auch neu bei Schallköpfen, entschuldige die möglicherweise unklare Antwort.


1

Ich finde, dieser Beitrag gibt Ihnen eine bessere Vogelperspektive auf den Schallkopf.

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


3
Von Antworten, die sich lediglich auf externe Links stützen, wird von SO abgeraten, da die Links in Zukunft jederzeit unterbrochen werden können. Zitieren Sie stattdessen den Inhalt Ihrer Antwort.
Vincent Cantin

@ VincentCantin Tatsächlich wurde der mittlere Beitrag gelöscht.
Dmitri Zaitsev

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.