Qualcuno può spiegarmi i trasduttori Clojure in termini semplici?


100

Ho provato a leggere su questo ma ancora non capisco il valore di loro o cosa sostituiscono. E rendono il mio codice più breve, più comprensibile o cosa?

Aggiornare

Molte persone hanno pubblicato risposte, ma sarebbe bello vedere esempi di con e senza trasduttori per qualcosa di molto semplice, che anche un idiota come me può capire. A meno che, ovviamente, i trasduttori non abbiano bisogno di un certo livello di comprensione elevato, nel qual caso non li capirò mai :(

Risposte:


75

I trasduttori sono ricette su cosa fare con una sequenza di dati senza sapere quale sia la sequenza sottostante (come farlo). Può essere qualsiasi seq, canale asincrono o forse osservabile.

Sono componibili e polimorfici.

Il vantaggio è che non è necessario implementare tutti i combinatori standard ogni volta che viene aggiunta una nuova origine dati. Ancora e ancora. Come effetto risultante, tu come utente puoi riutilizzare quelle ricette su diverse fonti di dati.

Aggiornamento dell'annuncio

Prima della versione 1.7 di Clojure c'erano tre modi per scrivere query sul flusso di dati:

  1. chiamate annidate
    (reduce + (filter odd? (map #(+ 2 %) (range 0 10))))
  1. composizione funzionale
    (def xform
      (comp
        (partial filter odd?)
        (partial map #(+ 2 %))))
    (reduce + (xform (range 0 10)))
  1. macro di threading
    (defn xform [xs]
      (->> xs
           (map #(+ 2 %))
           (filter odd?)))
    (reduce + (xform (range 0 10)))

Con i trasduttori lo scriverai come:

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

Fanno tutti lo stesso. La differenza è che non chiami mai i trasduttori direttamente, ma li passi a un'altra funzione. I trasduttori sanno cosa fare, la funzione che riceve il trasduttore sa come. L'ordine dei combinatori è come lo scrivi con la macro di threading (ordine naturale). Ora puoi riutilizzare xformcon il canale:

(chan 1 xform)

3
Stavo più cercando una risposta che venga fornita con un esempio che mi mostri come i trasduttori mi fanno risparmiare tempo.
appshare.co

Non lo fanno se non sei Clojure o un manutentore di librerie di flussi di dati.
Aleš Roubíček

5
Non è una decisione tecnica. Usiamo solo decisioni basate sul valore aziendale. "
Usali e

1
Potresti avere più difficoltà a mantenere il tuo lavoro se ritardi nel tentativo di utilizzare i trasduttori fino al rilascio di Clojure 1.7.
user100464

7
I trasduttori sembrano essere un modo utile per astrarre su varie forme di oggetti iterabili. Questi possono essere non consumabili, come le sequenze Clojure, o consumabili (come i canali asincroni). A questo proposito, mi sembra che trarreste grandi vantaggi dall'uso dei trasduttori se, ad esempio, passate da un'implementazione basata su seq a un'implementazione core.async usando i canali. I trasduttori dovrebbero consentirti di mantenere invariato il nucleo della tua logica. Utilizzando la tradizionale elaborazione basata su sequenze, dovresti convertirla per utilizzare trasduttori o alcuni analoghi core-asincroni. Questo è il business case.
Nathan Davis

47

I trasduttori migliorano l'efficienza e consentono di scrivere codice efficiente in modo più modulare.

Questa è una prova decente .

Rispetto alla composizione di chiamate al vecchio map, ecc. filter, reduceSi ottengono prestazioni migliori perché non è necessario creare raccolte intermedie tra ogni passaggio e percorrerle ripetutamente.

Rispetto reducerso componendo manualmente tutte le operazioni in un'unica espressione, è possibile utilizzare più facilmente le astrazioni, migliorare la modularità e il riutilizzo delle funzioni di elaborazione.


2
Solo curioso, hai detto sopra: "costruire collezioni intermedie tra ogni passaggio". Ma le "raccolte intermedie" non suonano come un anti-pattern? .NET offre enumerabili pigri, Java offre flussi pigri o iterabili guidati da Guava, anche il pigro Haskell deve avere qualcosa di pigro. Nessuno di questi richiede map/ reduceper utilizzare raccolte intermedie perché tutte costruiscono una catena di iteratori. Dove mi sbaglio qui?
Lyubomyr Shaydariv

3
Clojure mape filtercrea raccolte intermedie quando nidificate.
Noisesmith

4
E almeno per quanto riguarda la versione della pigrizia di Clojure, la questione della pigrizia è ortogonale qui. Sì, mappa e filtro sono pigri, generano anche contenitori per valori pigri quando li concatenate. Se non ti aggrappi alla testa, non costruisci grandi sequenze pigre che non sono necessarie, ma costruisci comunque quelle astrazioni intermedie per ogni elemento pigro.
Noisesmith

Un esempio sarebbe carino.
appshare.co

8
@LyubomyrShaydariv Con "raccolta intermedia", noisesmith non significa "itera / reifica un'intera collezione, quindi itera / reifica un'altra intera collezione". Lui o lei significa che quando annidate chiamate di funzione che restituiscono sequenziali, ogni chiamata di funzione risulta nella creazione di una nuova sequenza. L'iterazione effettiva avviene ancora una sola volta, ma c'è un consumo di memoria aggiuntivo e un'allocazione di oggetti a causa delle sequenziali annidate.
erikprice

22

I trasduttori sono un mezzo di combinazione per ridurre le funzioni.

Esempio: le funzioni di riduzione sono funzioni che accettano due argomenti: un risultato fino a quel momento e un input. Restituiscono un nuovo risultato (finora). Ad esempio +: con due argomenti, puoi pensare al primo come risultato fino a quel momento e al secondo come input.

Un trasduttore ora potrebbe prendere la funzione + e renderla una funzione doppia (raddoppia ogni ingresso prima di aggiungerlo). Ecco come sarebbe il trasduttore (in termini più elementari):

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

Per l'illustrazione sostituire rfncon +per vedere come +si trasforma in due volte più:

(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

Così

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

ora produrrebbe 12.

Le funzioni riducenti restituite dai trasduttori sono indipendenti da come viene accumulato il risultato perché si accumulano con la funzione riducente passata loro, inconsapevolmente come. Qui usiamo conjinvece di +. Conjaccetta una raccolta e un valore e restituisce una nuova raccolta con quel valore aggiunto.

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

produrrebbe [2 4 6]

Sono anche indipendenti dal tipo di sorgente in ingresso.

Più trasduttori possono essere concatenati come una ricetta (concatenabile) per trasformare le funzioni di riduzione.

Aggiornamento: poiché ora esiste una pagina ufficiale a riguardo, consiglio vivamente di leggerlo: http://clojure.org/transducers


Bella spiegazione, ma presto entrato nel gergo troppo lungo per me, "Le funzioni di riduzione generate dai trasduttori sono indipendenti da come si accumula il risultato".
appshare.co

1
Hai ragione, la parola generata qui era inappropriata.
Leon Grapenthin

Va bene. Comunque capisco che i trasformatori sono solo un'ottimizzazione ora, quindi probabilmente non dovrebbero essere usati comunque
appshare.co

1
Sono un mezzo di combinazione per ridurre le funzioni. In quale altro posto l'hai? Questo è molto più di un'ottimizzazione.
Leon Grapenthin

Trovo questa risposta molto interessante, ma non mi è chiaro come si colleghi ai trasduttori (anche perché trovo ancora l'argomento confuso). Qual è la relazione tra doubleetransduce ?
Marte

21

Supponi di voler utilizzare una serie di funzioni per trasformare un flusso di dati. La shell Unix ti consente di fare questo genere di cose con l'operatore pipe, ad es

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

(Il comando precedente conta il numero di utenti con la lettera r in maiuscolo o minuscolo nel loro nome utente). Questo è implementato come un insieme di processi, ognuno dei quali legge dall'output dei processi precedenti, quindi ci sono quattro flussi intermedi. Potresti immaginare un'implementazione diversa che componga i cinque comandi in un unico comando aggregato, che leggerà dal suo input e scriverà il suo output esattamente una volta. Se i flussi intermedi fossero costosi e la composizione fosse economica, potrebbe essere un buon compromesso.

Lo stesso genere di cose vale per Clojure. Esistono diversi modi per esprimere una pipeline di trasformazioni, ma a seconda di come lo fai, puoi finire con flussi intermedi che passano da una funzione all'altra. Se hai molti dati, è più veloce comporre quelle funzioni in una singola funzione. I trasduttori lo rendono facile. Un'innovazione precedente di Clojure, i riduttori, ti consente di farlo anche tu, ma con alcune limitazioni. I trasduttori rimuovono alcune di queste restrizioni.

Quindi, per rispondere alla tua domanda, i trasduttori non renderanno necessariamente il tuo codice più breve o più comprensibile, ma probabilmente il tuo codice non sarà nemmeno più lungo o meno comprensibile, e se stai lavorando con molti dati, i trasduttori possono creare il tuo codice Più veloce.

Questa è una panoramica abbastanza buona dei trasduttori.


1
Ah, quindi i trasduttori sono principalmente un'ottimizzazione delle prestazioni, è questo che stai dicendo?
appshare.co

@Zubair Sì, è vero. Notare che l'ottimizzazione va oltre l'eliminazione dei flussi intermedi; potresti anche essere in grado di eseguire operazioni in parallelo.
user100464

2
Vale la pena menzionare pmap, che non sembra ricevere abbastanza attenzione. Se si mapesegue il ping di una funzione costosa su una sequenza, rendere l'operazione parallela è facile come aggiungere "p". Non è necessario modificare nient'altro nel codice ed è ora disponibile: non alpha, non beta. (Se la funzione crea sequenze intermedie, i trasduttori potrebbero essere più veloci, immagino.)
Marte

10

Rich Hickey ha tenuto un discorso "Transducers" alla conferenza Strange Loop 2014 (45 min).

Spiega in modo semplice cosa sono i trasduttori, con esempi del mondo reale: processare i bagagli in un aeroporto. Separa chiaramente i diversi aspetti e li mette in contrasto con gli approcci attuali. Verso la fine, fornisce la motivazione della loro esistenza.

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


8

Ho trovato esempi di lettura da trasduttori-js mi aiuta a capirli in termini concreti su come potrei usarli nel codice quotidiano.

Ad esempio, considera questo esempio (tratto dal README al link sopra):

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]

Per uno, l'utilizzo xfsembra molto più pulito della solita alternativa con Underscore.

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

Come mai l'esempio dei trasduttori è molto più lungo. La versione di sottolineatura sembra molto più concisa
appshare.co

1
@Zubair Non propriot.into([], t.comp(t.map(inc), t.filter(isEven)), [0,1,2,3,4])
Juan Castañeda

7

I trasduttori sono (per quanto ne so!) Funzioni che prendono una funzione riducente e ne restituiscono un'altra. Una funzione riducente è quella che

Per esempio:

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 questo caso il mio trasduttore assume una funzione di filtraggio degli ingressi che applica a 0 quindi se quel valore è pari? nel primo caso il filtro passa quel valore al contatore, poi filtra il valore successivo. Invece di filtrare prima e poi passare tutti questi valori al conteggio.

È la stessa cosa nel secondo esempio controlla un valore alla volta e se quel valore è inferiore a 3 allora lascia che count aggiunga 1.


Mi è piaciuta questa semplice spiegazione
Ignacio

7

Una chiara definizione del trasduttore è qui:

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.

Per capirlo, consideriamo il seguente semplice esempio:

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

Che ne dici di sapere quanti bambini ci sono nel villaggio? Possiamo scoprirlo facilmente con il seguente riduttore:

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

Ecco un altro modo per farlo:

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

Inoltre, è davvero potente quando si prendono in considerazione anche i sottogruppi. Ad esempio, se vogliamo sapere quanti bambini ci sono nella famiglia Brown, possiamo eseguire:

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

Spero che tu possa trovare utili questi esempi. Puoi trovare di più qui

Spero che sia d'aiuto.

Clemencio Morales Lucas.


3
"I trasduttori sono un modo potente e componibile per costruire trasformazioni algoritmiche che puoi riutilizzare in molti contesti, e stanno arrivando a Clojure core e core.async." definizione potrebbe valere per quasi tutto?
appshare.co

1
A quasi tutti i trasduttori Clojure, direi.
Clemencio Morales Lucas

6
È più una dichiarazione di intenti che una definizione.
Marte

4

Ne ho parlato in un blog con un esempio clojurescript che spiega come le funzioni di sequenza sono ora estensibili essendo in grado di sostituire la funzione di riduzione.

Questo è il punto dei trasduttori mentre lo leggo. Se si pensa all'operazione consor conjche è codificata in operazioni come map, filterecc., La funzione di riduzione era irraggiungibile.

Con i trasduttori, la funzione di riduzione è disaccoppiata e posso sostituirla come ho fatto con l'array javascript nativo pushgrazie ai trasduttori.

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

filter e gli amici hanno una nuova operazione 1 arità che restituirà una funzione di trasduzione che è possibile utilizzare per fornire la propria funzione di riduzione.


4

Ecco il mio (principalmente) gergo e risposta senza codice.

Pensa ai dati in due modi, uno stream (valori che si verificano nel tempo come gli eventi) o una struttura (dati che esistono in un punto nel tempo come un elenco, un vettore, un array, ecc.).

Ci sono alcune operazioni che potresti voler eseguire su stream o strutture. Una di queste operazioni è la mappatura. Una funzione di mappatura potrebbe incrementare ogni elemento di dati (supponendo che sia un numero) di 1 e si può sperare di immaginare come questo potrebbe applicarsi a uno stream oa una struttura.

Una funzione di mappatura è solo una di una classe di funzioni a volte denominate "funzioni di riduzione". Un'altra funzione di riduzione comune è il filtro che rimuove i valori che corrispondono a un predicato (ad es. Rimuove tutti i valori pari).

I trasduttori consentono di "avvolgere" una sequenza di una o più funzioni riducenti e di produrre un "pacchetto" (che è esso stesso una funzione) che funziona su entrambi i flussi o le strutture. Ad esempio, potresti "impacchettare" una sequenza di funzioni di riduzione (ad es. Filtrare i numeri pari, quindi mappare i numeri risultanti per incrementarli di 1) e quindi utilizzare il "pacchetto" del trasduttore su un flusso o una struttura di valori (o entrambi) .

Quindi cosa c'è di speciale in questo? In genere, le funzioni di riduzione non possono essere composte in modo efficiente per lavorare su flussi e strutture.

Quindi il vantaggio per te è che puoi sfruttare le tue conoscenze su queste funzioni e applicarle a più casi d'uso. Il costo per te è che devi imparare alcuni macchinari extra (cioè il trasduttore) per darti questa potenza extra.


2

Per quanto ne so, sono come blocchi di costruzione , disaccoppiati dall'implementazione di input e output. Devi solo definire l'operazione.

Poiché l'implementazione dell'operazione non è nel codice dell'ingresso e non viene eseguita alcuna operazione sull'uscita, i trasduttori sono estremamente riutilizzabili. Mi ricordano Flow in Akka Streams .

Sono anche nuovo per i trasduttori, mi scuso per la risposta forse poco chiara.


1

Trovo che questo post ti dia una visione più a volo d'uccello del trasduttore.

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


3
Le risposte che si basano semplicemente su collegamenti esterni sono sconsigliate su SO poiché i collegamenti potrebbero interrompersi in qualsiasi momento in futuro. Cita invece il contenuto nella tua risposta.
Vincent Cantin

@VincentCantin In effetti, il post Medium è stato cancellato.
Dmitri Zaitsev

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.