¿Puede alguien explicar Clojure Transducers en términos simples?
(12)
He intentado leer sobre esto pero todavía no entiendo su valor o lo que reemplazan. ¿Y hacen que mi código sea más corto, más comprensible o qué?
Actualizar
Muchas personas publicaron respuestas, pero sería bueno ver ejemplos de con y sin transductores para algo muy simple, que incluso un idiota como yo puede entender. A menos que, por supuesto, los transductores necesiten un alto nivel de comprensión, en cuyo caso nunca los entenderé :(
Aquí está mi (sobre todo) jerga y código de respuesta gratuita.
Piense en los datos de dos maneras, una secuencia (valores que ocurren a lo largo del tiempo, como los eventos) o una estructura (datos que existen en un momento dado, como una lista, un vector, una matriz, etc.).
Hay ciertas operaciones que puede desear realizar sobre secuencias o estructuras. Una de esas operaciones es mapeo. Una función de mapeo puede incrementar cada elemento de datos (asumiendo que es un número) por 1 y con suerte puede imaginar cómo esto podría aplicarse a una secuencia o una estructura.
Una función de mapeo es solo una de una clase de funciones que a veces se denominan "funciones reductoras". Otra función de reducción común es el filtro que elimina valores que coinciden con un predicado (por ejemplo, eliminar todos los valores que son pares).
Los transductores le permiten "ajustar" una secuencia de una o más funciones reductoras y producir un "paquete" (que a su vez es una función) que funciona tanto en secuencias como en estructuras. Por ejemplo, podría "empaquetar" una secuencia de funciones reductoras (por ejemplo, filtrar números pares, luego asignar los números resultantes para incrementarlos en 1) y luego usar ese "paquete" de transductores en una secuencia o estructura de valores (o ambos) .
Entonces, ¿qué tiene de especial esto? Por lo general, la reducción de funciones no puede ser compuesta eficientemente para trabajar tanto en las secuencias como en las estructuras.
Entonces, el beneficio para usted es que puede aprovechar su conocimiento sobre estas funciones y aplicarlas a más casos de uso. El costo para usted es que tiene que aprender un poco de maquinaria adicional (es decir, el transductor) para darle este poder extra.
Encuentro que esta publicación te da una vista más panorámica del transductor.
https://medium.com/@roman01la/understanding-transducers-in-javascript-3500d3bd9624
He encontrado que leer ejemplos de transducers-js me ayuda a entenderlos en términos concretos de cómo podría usarlos en el código diario.
Por ejemplo, considere este ejemplo (tomado del archivo README en el enlace de arriba):
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]
Por un lado, usar xf
parece mucho más limpio que la alternativa habitual con Underscore.
_.filter(_.map([0, 1, 2, 3, 4], inc), isEven);
Los transductores mejoran la eficiencia y le permiten escribir códigos eficientes de una manera más modular.
En comparación con la composición de llamadas al antiguo map
, filter
, reduce
, etc. obtiene un mejor rendimiento porque no necesita crear colecciones intermedias entre cada paso, y camina repetidamente por esas colecciones.
Comparado con los reducers
, o componiendo manualmente todas tus operaciones en una sola expresión, obtienes más abstracciones, mejor modularidad y reutilización de las funciones de procesamiento.
Los transductores son (¡a mi entender!) Funciones que toman una función reductora y devuelven otra. Una función reductora es una que
Por ejemplo:
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
En este caso, my-transducer toma una función de filtrado de entrada que se aplica a 0, entonces, ¿ese valor es par? en el primer caso, el filtro pasa ese valor al contador, luego filtra el siguiente valor. En lugar de primero filtrar y luego pasar todos esos valores para contar.
Es lo mismo en el segundo ejemplo que verifica un valor a la vez y si ese valor es menor que 3, entonces permite contar 1.
Los transductores son recetas de qué hacer con una secuencia de datos sin saber cuál es la secuencia subyacente (cómo hacerlo). Puede ser cualquier canal seq, asincrónico o tal vez observable.
Son compostables y polimórficos.
El beneficio es que no tiene que implementar todos los combinadores estándar cada vez que se agrega una nueva fuente de datos. Una y otra vez. Como efecto resultante, usted, como usuario, puede reutilizar esas recetas en diferentes fuentes de datos.
Actualización de anuncios
La versión anterior 1.7 de Clojure tenía tres formas de escribir consultas de flujo de datos:
- llamadas anidadas
(reduce + (filter odd? (map #(+ 2 %) (range 0 10))))
- composición funcional
(def xform
(comp
(partial filter odd?)
(partial map #(+ 2 %))))
(reduce + (xform (range 0 10)))
- threading macro
(defn xform [xs]
(->> xs
(map #(+ 2 %))
(filter odd?)))
(reduce + (xform (range 0 10)))
Con transductores lo escribirás como:
(def xform
(comp
(map #(+ 2 %))
(filter odd?)))
(transduce xform + (range 0 10))
Todos hacen lo mismo. La diferencia es que nunca llamas Transductores directamente, los pasas a otra función. Los transductores saben qué hacer, la función que obtiene el transductor lo sabe. El orden de los combinadores es como si lo escribieras con macro de enhebrado (orden natural). Ahora puedes reutilizar xform
con el canal:
(chan 1 xform)
Los transductores son un medio de combinación para reducir funciones.
Ejemplo: Las funciones de reducción son funciones que toman dos argumentos: un resultado hasta el momento y una entrada. Devuelven un nuevo resultado (hasta ahora). Por ejemplo, +
: con dos argumentos, puede pensar en el primero como resultado hasta ahora y en el segundo como entrada.
Un transductor ahora podría tomar la función + y convertirla en una función dos veces más (duplica cada entrada antes de agregarla). Así es como se vería ese transductor (en la mayoría de los términos básicos):
(defn double
[rfn]
(fn [r i]
(rfn r (* 2 i))))
Para la ilustración, sustituya rfn
por +
para ver cómo +
se transforma en dos veces más:
(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
Asi que
(reduce (double +) 0 [1 2 3])
ahora rendiría 12.
Las funciones de reducción devueltas por los transductores son independientes de cómo se acumula el resultado porque se acumulan con la función de reducción que se les transfiere, sin saber cómo. Aquí usamos conj
lugar de +
. Conj
toma una colección y un valor y devuelve una nueva colección con ese valor adjunto.
(reduce (double conj) [] [1 2 3])
rendiría [2 4 6]
También son independientes de qué tipo de fuente es la entrada.
Se pueden encadenar múltiples transductores como una receta (encadenable) para transformar las funciones reductoras.
Actualización: ya que ahora hay una página oficial al respecto, recomiendo leerlo: http://clojure.org/transducers
Por lo que yo entiendo, son como bloques de construcción , desacoplados de la implementación de entrada y salida. Usted solo define la operación.
Como la implementación de la operación no está en el código de entrada y no se hace nada con la salida, los transductores son extremadamente reutilizables. Me recuerdan a Flow s en Akka Streams .
También soy nuevo en los transductores, lo siento por la respuesta posiblemente confusa.
Publiqué sobre esto con un example clojurescript que explica cómo las funciones de secuencia ahora son extensibles al poder reemplazar la función de reducción.
Este es el punto de los transductores a medida que lo leo. Si piensas en la operación cons
o conj
que está codificada en operaciones como map
, filter
, etc., la función reductora era inalcanzable.
Con los transductores, la función reductora está desacoplada y puedo reemplazarla como lo hice con el arrastre nativo de javascript gracias a los transductores.
(transduce (filter #(not (.hasOwnProperty prevChildMapping %))) (.-push #js[]) #js [] nextKeys)
filter
y amigos tienen una nueva operación de 1 aridad que devolverá una función de transducción que puede usar para suministrar su propia función de reducción.
Rich Hickey dio una charla de ''Transductores'' en la conferencia Strange Loop 2014 (45 min).
Explica de manera sencilla qué son los transductores, con ejemplos del mundo real: procesar bolsas en un aeropuerto. Él claramente separa los diferentes aspectos y los contrasta con los enfoques actuales. Hacia el final, él da la razón de su existencia.
Supongamos que quiere usar una serie de funciones para transformar una secuencia de datos. El shell Unix le permite hacer este tipo de cosas con el operador de tubería, por ejemplo
cat /etc/passwd | tr ''[:lower:]'' ''[:upper:]'' | cut -d: -f1| grep R| wc -l
(El comando anterior cuenta el número de usuarios con la letra r en mayúscula o minúscula en su nombre de usuario). Esto se implementa como un conjunto de procesos, cada uno de los cuales se lee a partir de los resultados de los procesos anteriores, por lo que hay cuatro flujos intermedios. Se podría imaginar una implementación diferente que componga los cinco comandos en un solo comando agregado, que leería de su entrada y escribiría su salida exactamente una vez. Si las transmisiones intermedias son caras y la composición es barata, podría ser una buena solución de compromiso.
El mismo tipo de cosas son válidas para Clojure. Hay formas múltiples y concisas de expresar una serie de transformaciones, pero dependiendo de cómo lo haga, puede terminar con flujos intermedios que pasan de una función a la siguiente. Si tiene muchos datos, es más rápido componer esas funciones en una sola función. Los transductores hacen que sea más fácil hacer eso. Una innovación anterior de Clojure, reductores, le permite hacer eso también, pero con algunas restricciones. Los transductores eliminan algunas de esas restricciones.
Entonces, para responder a su pregunta, los transductores no necesariamente harán que su código sea más corto o más comprensible, pero su código probablemente tampoco será más extenso o menos comprensible, y si está trabajando con una gran cantidad de datos, los transductores pueden hacer su código Más rápido.
This es una muy buena descripción general de los transductores.
Una clara definición de transductor está aquí:
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.
Para entenderlo, consideremos el siguiente ejemplo simple:
;; 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é tal si queremos saber cuántos niños hay en el pueblo? Podemos encontrarlo fácilmente con el siguiente reductor:
;; 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
Aquí hay otra manera de hacerlo:
;; 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
Además, es realmente poderoso cuando se toman subgrupos en la cuenta también. Por ejemplo, si quisiéramos saber cuántos niños hay en la familia Brown, podemos ejecutar:
;; 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
Espero que puedas encontrar útiles estos ejemplos. Puedes encontrar más here
Espero eso ayude.
Clemencio Morales Lucas.