Quelqu'un peut-il m'expliquer les transducteurs Clojure en termes simples?


100

J'ai essayé de lire à ce sujet mais je ne comprends toujours pas leur valeur ou ce qu'ils remplacent. Et rendent-ils mon code plus court, plus compréhensible ou quoi?

Mettre à jour

Beaucoup de gens ont posté des réponses, mais ce serait bien de voir des exemples de transducteurs avec et sans transducteurs pour quelque chose de très simple, que même un idiot comme moi peut comprendre. À moins bien sûr que les transducteurs aient besoin d'un certain niveau de compréhension élevé, auquel cas je ne les comprendrai jamais :(

Réponses:


75

Les transducteurs sont des recettes sur ce qu'il faut faire avec une séquence de données sans savoir quelle est la séquence sous-jacente (comment le faire). Il peut s'agir de n'importe quel canal séquentiel, asynchrone ou peut-être observable.

Ils sont composables et polymorphes.

L'avantage est que vous n'avez pas à implémenter tous les combinateurs standard à chaque fois qu'une nouvelle source de données est ajoutée. Encore et encore. En tant qu'utilisateur, vous pouvez réutiliser ces recettes sur différentes sources de données.

Mise à jour de l'annonce

Avant la version 1.7 de Clojure, vous disposiez de trois façons d'écrire des requêtes de flux de données:

  1. appels imbriqués
    (reduce + (filter odd? (map #(+ 2 %) (range 0 10))))
  1. composition fonctionnelle
    (def xform
      (comp
        (partial filter odd?)
        (partial map #(+ 2 %))))
    (reduce + (xform (range 0 10)))
  1. macro de filetage
    (defn xform [xs]
      (->> xs
           (map #(+ 2 %))
           (filter odd?)))
    (reduce + (xform (range 0 10)))

Avec des transducteurs, vous l'écrivez comme:

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

Ils font tous la même chose. La différence est que vous n'appelez jamais directement les transducteurs, vous les passez à une autre fonction. Les transducteurs savent quoi faire, la fonction qui obtient le transducteur sait comment. L'ordre des combinateurs est comme vous l'écrivez avec une macro de threading (ordre naturel). Vous pouvez maintenant réutiliser xformavec le canal:

(chan 1 xform)

3
Je cherchais plutôt une réponse accompagnée d'un exemple qui me montre comment les transducteurs me font gagner du temps.
appshare.co

Ils ne le font pas si vous n'êtes pas Clojure ou un responsable de la bibliothèque de flux de données.
Aleš Roubíček

5
Ce n'est pas une décision technique. Nous n'utilisons que des décisions basées sur la valeur commerciale. "Il suffit de les utiliser" me fera virer
appshare.co

1
Vous aurez peut-être plus de facilité à garder votre travail si vous retardez l'utilisation des transducteurs jusqu'à ce que Clojure 1.7 soit libéré.
user100464

7
Les transducteurs semblent être un moyen utile d'abstraire diverses formes d'objets itérables. Ceux-ci peuvent être non consommables, tels que les séquences Clojure, ou consommables (tels que les canaux asynchrones). À cet égard, il me semble que vous bénéficieriez grandement de l'utilisation de transducteurs si, par exemple, vous passez d'une implémentation basée sur seq à une implémentation core.async utilisant des canaux. Les transducteurs devraient vous permettre de garder le cœur de votre logique inchangé. En utilisant le traitement traditionnel basé sur des séquences, vous devrez le convertir pour utiliser des transducteurs ou un analogique core-async. C'est l'analyse de rentabilisation.
Nathan Davis

47

Les transducteurs améliorent l'efficacité et vous permettent d'écrire du code efficace de manière plus modulaire.

C'est un parcours décent .

Par rapport à la composition des appels à l'ancienne map, filter, reduceetc. vous obtenez une meilleure performance parce que vous ne avez pas besoin de construire des collections intermédiaires entre chaque étape, et à plusieurs reprises ces collections marcher.

Par rapport à reducers, ou en composant manuellement toutes vos opérations en une seule expression, vous obtenez plus facilement des abstractions, une meilleure modularité et une réutilisation des fonctions de traitement.


2
Juste curieux, vous avez dit plus haut: "construire des collections intermédiaires entre chaque étape". Mais les "collections intermédiaires" ne ressemblent-elles pas à un anti-pattern? .NET propose des énumérables paresseux, Java propose des flux paresseux ou des itérables pilotés par Guava, Haskell paresseux doit aussi avoir quelque chose de paresseux. Aucun de ceux-ci ne nécessite map/ reduced'utiliser des collections intermédiaires car tous construisent une chaîne d'itérateurs. Où ai-je tort ici?
Lyubomyr Shaydariv

3
Clojure mapet filtercrée des collections intermédiaires lorsqu'elles sont imbriquées.
noisesmith

4
Et au moins en ce qui concerne la version de Clojure de la paresse, la question de la paresse est ici orthogonale. Oui, la carte et le filtre sont paresseux, ils génèrent également des conteneurs pour les valeurs paresseuses lorsque vous les chaînez. Si vous ne tenez pas la tête, vous ne construisez pas de grandes séquences paresseuses qui ne sont pas nécessaires, mais vous construisez toujours ces abstractions intermédiaires pour chaque élément paresseux.
noisesmith

Un exemple serait bien.
appshare.co

8
@LyubomyrShaydariv Par "collection intermédiaire", noisesmith ne signifie pas "itérer / réifier une collection entière, puis itérer / réifier une autre collection entière". Il ou elle signifie que lorsque vous imbriquez des appels de fonction qui renvoient des séquentiels, chaque appel de fonction entraîne la création d'un nouveau séquentiel. L'itération réelle n'a lieu qu'une seule fois, mais il y a une consommation de mémoire et une allocation d'objets supplémentaires en raison des séquentiels imbriqués.
erikprice

22

Les transducteurs sont un moyen de combinaison pour réduire les fonctions.

Exemple: les fonctions de réduction sont des fonctions qui prennent deux arguments: un résultat jusqu'à présent et une entrée. Ils renvoient un nouveau résultat (pour l'instant). Par exemple +: avec deux arguments, vous pouvez considérer le premier comme le résultat jusqu'à présent et le second comme l'entrée.

Un transducteur peut maintenant prendre la fonction + et en faire une fonction deux fois plus (double chaque entrée avant de l'ajouter). Voici à quoi ressemblerait ce transducteur (dans les termes les plus élémentaires):

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

Pour l'illustration, remplacez rfnpar +pour voir comment +se transforme en deux fois plus:

(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

Alors

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

donnerait maintenant 12.

Les fonctions de réduction renvoyées par les transducteurs sont indépendantes de la façon dont le résultat est accumulé car elles s'accumulent avec la fonction de réduction qui leur est transmise, sans le savoir comment. Ici, nous utilisons à la conjplace de +. Conjprend une collection et une valeur et retourne une nouvelle collection avec cette valeur ajoutée.

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

donnerait [2 4 6]

Ils sont également indépendants du type de source d'entrée.

Plusieurs transducteurs peuvent être chaînés en tant que recette (chaînable) pour transformer les fonctions réductrices.

Mise à jour: Puisqu'il existe maintenant une page officielle à ce sujet, je recommande vivement de la lire: http://clojure.org/transducers


Belle explication mais vite entré dans trop de jargon pour moi, "Les fonctions de réduction générées par les transducteurs sont indépendantes de la façon dont le résultat est accumulé".
appshare.co

1
Vous avez raison, le mot généré était inapproprié ici.
Leon Grapenthin

C'est bon. Quoi qu'il en soit, je comprends que les transformateurs ne sont qu'une optimisation maintenant, donc ils ne devraient probablement pas être utilisés de toute façon
appshare.co

1
Ils sont un moyen de combinaison pour réduire les fonctions. Où avez-vous cela? C'est bien plus qu'une optimisation.
Leon Grapenthin

Je trouve cette réponse très intéressante, mais je ne vois pas clairement comment elle se connecte aux transducteurs (en partie parce que je trouve toujours le sujet déroutant). Quelle est la relation entre doubleet transduce?
Mars

21

Supposons que vous souhaitiez utiliser une série de fonctions pour transformer un flux de données. Le shell Unix vous permet de faire ce genre de chose avec l'opérateur pipe, par exemple

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

(La commande ci-dessus compte le nombre d'utilisateurs avec la lettre r en majuscule ou en minuscule dans leur nom d'utilisateur). Ceci est implémenté comme un ensemble de processus, dont chacun lit à partir de la sortie des processus précédents, il y a donc quatre flux intermédiaires. Vous pouvez imaginer une implémentation différente qui compose les cinq commandes en une seule commande d'agrégation, qui lirait à partir de son entrée et écrirait sa sortie exactement une fois. Si les flux intermédiaires étaient chers et la composition bon marché, cela pourrait être un bon compromis.

Le même genre de chose vaut pour Clojure. Il existe plusieurs façons d'exprimer un pipeline de transformations, mais selon la façon dont vous le faites, vous pouvez vous retrouver avec des flux intermédiaires passant d'une fonction à l'autre. Si vous avez beaucoup de données, il est plus rapide de composer ces fonctions en une seule fonction. Les transducteurs facilitent cette tâche. Une innovation Clojure antérieure, les réducteurs, vous permet de le faire aussi, mais avec quelques restrictions. Les transducteurs suppriment certaines de ces restrictions.

Donc, pour répondre à votre question, les transducteurs ne rendront pas nécessairement votre code plus court ou plus compréhensible, mais votre code ne sera probablement pas plus long ou moins compréhensible non plus, et si vous travaillez avec beaucoup de données, les transducteurs peuvent rendre votre code plus rapide.

C'est un assez bon aperçu des transducteurs.


1
Ah, donc les transducteurs sont principalement une optimisation des performances, c'est ce que vous dites?
appshare.co

@Zubair Oui, c'est vrai. Notez que l'optimisation va au-delà de l'élimination des flux intermédiaires; vous pourrez peut-être également effectuer des opérations en parallèle.
user100464

2
Cela vaut la peine d'être mentionné pmap, ce qui ne semble pas attirer suffisamment l'attention. Si vous envoyez mapun ping à une fonction coûteuse sur une séquence, rendre l'opération parallèle est aussi simple que d'ajouter "p". Pas besoin de changer quoi que ce soit d'autre dans votre code, et il est disponible maintenant - pas alpha, pas bêta. (Si la fonction crée des séquences intermédiaires, les transducteurs pourraient être plus rapides, je suppose.)
Mars

10

Rich Hickey a donné une conférence «Transducers» lors de la conférence Strange Loop 2014 (45 min).

Il explique de manière simple ce que sont les transducteurs, avec des exemples du monde réel - le traitement des sacs dans un aéroport. Il sépare clairement les différents aspects et les met en contraste avec les approches actuelles. Vers la fin, il donne la justification de leur existence.

Vidéo: https://www.youtube.com/watch?v=6mTbuzafcII


8

J'ai trouvé que la lecture d'exemples de transducers-js m'aide à les comprendre concrètement sur la façon dont je pourrais les utiliser dans le code au jour le jour.

Par exemple, considérons cet exemple (tiré du README au lien ci-dessus):

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]

D'une part, l'utilisation xfsemble beaucoup plus propre que l'alternative habituelle avec Underscore.

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

Comment se fait-il que l'exemple des transducteurs soit tellement plus long. La version de soulignement semble beaucoup plus concise
appshare.co

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

7

Les transducteurs sont (à ma connaissance!) Des fonctions qui prennent une fonction réductrice et en renvoient une autre. Une fonction réductrice est celle qui

Par exemple:

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

Dans ce cas mon-transducteur prend une fonction de filtrage d'entrée qu'il applique à 0 alors si cette valeur est paire? dans le premier cas, le filtre transmet cette valeur au compteur, puis il filtre la valeur suivante. Au lieu de filtrer d'abord, puis de passer toutes ces valeurs pour compter.

C'est la même chose dans le deuxième exemple, il vérifie une valeur à la fois et si cette valeur est inférieure à 3, alors il laisse compter ajouter 1.


J'ai aimé cette explication simple
Ignacio

7

Une définition claire du transducteur est ici:

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.

Pour le comprendre, considérons l'exemple simple suivant:

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

Qu'en est-il que nous voulons savoir combien d'enfants il y a dans le village? Nous pouvons facilement le trouver avec le réducteur suivant:

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

Voici une autre façon de le faire:

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

De plus, il est vraiment puissant lors de la prise en compte des sous-groupes. Par exemple, si nous voulons savoir combien d'enfants sont dans la famille Brown, nous pouvons exécuter:

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

J'espère que vous trouverez ces exemples utiles. Vous pouvez en trouver plus ici

J'espère que ça aide.

Clemencio Morales Lucas.


3
"Les transducteurs sont un moyen puissant et adaptable de créer des transformations algorithmiques que vous pouvez réutiliser dans de nombreux contextes, et ils arrivent à Clojure core et core.async." définition pourrait s'appliquer à presque tout?
appshare.co

1
À presque tous les transducteurs Clojure, je dirais.
Clemencio Morales Lucas

6
C'est plus un énoncé de mission qu'une définition.
Mars

4

J'ai blogué à ce sujet avec un exemple clojurescript qui explique comment les fonctions de séquence sont maintenant extensibles en pouvant remplacer la fonction de réduction.

C'est le but des transducteurs tel que je le lis. Si vous pensez à l' opération consou conjqui est codée en dur dans des opérations telles que map, filteretc., la fonction de réduction était inaccessible.

Avec les transducteurs, la fonction réductrice est découplée et je peux la remplacer comme je l'ai fait avec le tableau natif javascript pushgrâce à des transducteurs.

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

filter et les amis ont une nouvelle opération 1 arity qui renverra une fonction de transduction que vous pouvez utiliser pour fournir votre propre fonction de réduction.


4

Voici ma réponse (principalement) sans jargon et sans code.

Pensez aux données de deux manières, un flux (des valeurs qui surviennent au fil du temps comme des événements) ou une structure (des données qui existent à un moment donné comme une liste, un vecteur, un tableau, etc.).

Vous pouvez souhaiter effectuer certaines opérations sur des flux ou des structures. Une de ces opérations est la cartographie. Une fonction de mappage peut incrémenter chaque élément de données (en supposant qu'il s'agit d'un nombre) de 1 et vous pouvez, espérons-le, imaginer comment cela pourrait s'appliquer à un flux ou à une structure.

Une fonction de mappage n'est qu'une des classes de fonctions parfois appelées "fonctions réductrices". Une autre fonction de réduction courante est le filtre qui supprime les valeurs qui correspondent à un prédicat (par exemple, supprime toutes les valeurs paires).

Les transducteurs vous permettent de "envelopper" une séquence d'une ou plusieurs fonctions réductrices et de produire un "package" (qui est lui-même une fonction) qui fonctionne à la fois sur les flux ou les structures. Par exemple, vous pouvez «empaqueter» une séquence de fonctions réductrices (par exemple, filtrer les nombres pairs, puis mapper les nombres résultants pour les incrémenter de 1), puis utiliser ce «paquet» de transducteur sur un flux ou une structure de valeurs (ou les deux) .

Alors, quelle est la particularité de cela? En règle générale, les fonctions de réduction ne peuvent pas être efficacement composées pour fonctionner à la fois sur les flux et les structures.

L'avantage pour vous est que vous pouvez tirer parti de vos connaissances sur ces fonctions et les appliquer à plus de cas d'utilisation. Le coût pour vous est que vous devez apprendre quelques machines supplémentaires (par exemple le transducteur) pour vous donner cette puissance supplémentaire.


2

Pour autant que je sache, ils sont comme des blocs de construction , découplés de l'implémentation d'entrée et de sortie. Vous définissez simplement l'opération.

Comme la mise en œuvre de l'opération n'est pas dans le code de l'entrée et que rien n'est fait avec la sortie, les transducteurs sont extrêmement réutilisables. Ils me rappellent les Flow dans Akka Streams .

Je suis également nouveau dans les transducteurs, désolé pour la réponse peut-être peu claire.


1

Je trouve que cet article vous donne une vue plus à vol d'oiseau du transducteur.

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


3
Les réponses reposant uniquement sur des liens externes sont déconseillées sur SO car les liens peuvent se rompre à tout moment à l'avenir. Citez plutôt le contenu de votre réponse.
Vincent Cantin

@VincentCantin En fait, le message Medium a été supprimé.
Dmitri Zaitsev

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.