¿Alguien puede explicarme los transductores Clojure en términos simples?


100

He intentado leer sobre esto, pero todavía no entiendo el valor de ellos o lo que reemplazan. ¿Hacen que mi código sea más corto, más comprensible o qué?

Actualizar

Mucha gente publicó respuestas, pero sería bueno ver ejemplos 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é :(

Respuestas:


75

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

Antes de la versión 1.7 de Clojure, tenía tres formas de escribir consultas de flujo de datos:

  1. llamadas anidadas
    (reduce + (filter odd? (map #(+ 2 %) (range 0 10))))
  1. composición funcional
    (def xform
      (comp
        (partial filter odd?)
        (partial map #(+ 2 %))))
    (reduce + (xform (range 0 10)))
  1. macro de enhebrado
    (defn xform [xs]
      (->> xs
           (map #(+ 2 %))
           (filter odd?)))
    (reduce + (xform (range 0 10)))

Con los transductores lo escribirás así:

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

Todos hacen lo mismo. La diferencia es que nunca llamas a los transductores directamente, los pasas a otra función. Los transductores saben qué hacer, la función que obtiene el transductor sabe cómo. El orden de los combinadores es como si lo escribiera con macro de subprocesos (orden natural). Ahora puedes reutilizar xformcon el canal:

(chan 1 xform)

3
Estaba más buscando una respuesta que venga con un ejemplo que me muestre cómo los transductores me ahorran tiempo.
appshare.co

No lo hacen si no eres Clojure o algún mantenedor de lib de flujo de datos.
Aleš Roubíček

5
No es una decisión técnica. Solo utilizamos decisiones basadas en el valor comercial. "Solo
úsalas

1
Es posible que le resulte más fácil mantener su trabajo si se demora en intentar usar transductores hasta que se lance Clojure 1.7.
user100464

7
Los transductores parecen ser una forma útil de abstraer varias formas de objetos iterables. Estos pueden ser no consumibles, como Clojure seqs, o consumibles (como canales asíncronos). En este sentido, me parece que se beneficiaría enormemente del uso de transductores si, por ejemplo, cambia de una implementación basada en seq a una implementación core.async usando canales. Los transductores deberían permitirle mantener el núcleo de su lógica sin cambios. Con el procesamiento tradicional basado en secuencia, tendría que convertir esto para usar transductores o algún análogo de núcleo asíncrono. Ese es el caso de negocios.
Nathan Davis

47

Los transductores mejoran la eficiencia y le permiten escribir código eficiente de una manera más modular.

Este es un recorrido decente .

En comparación con la composición de las llamadas a la antigua map, filter,reduce etc. se obtiene un mejor rendimiento debido a que no es necesario para construir colecciones intermedias entre cada paso, y en repetidas ocasiones caminar esas colecciones.

En comparación con reducers, o componiendo manualmente todas sus operaciones en una sola expresión, es más fácil usar abstracciones, mejor modularidad y reutilización de funciones de procesamiento.


2
Solo por curiosidad, dijiste anteriormente: "para construir colecciones intermedias entre cada paso". ¿Pero las "colecciones intermedias" no suenan como un anti-patrón? .NET ofrece enumerables perezosos, Java ofrece flujos perezosos o iterables impulsados ​​por Guava, el perezoso Haskell también debe tener algo perezoso. Ninguno de estos requiere map/ reduceusar colecciones intermedias porque todos ellos construyen una cadena de iteradores. ¿Dónde me equivoco aquí?
Lyubomyr Shaydariv

3
Clojure mapy filtercrea colecciones intermedias cuando están anidadas.
noisesmith

4
Y al menos con respecto a la versión de pereza de Clojure, el tema de la pereza es ortogonal aquí. Sí, el mapa y el filtro son perezosos, también generan contenedores para valores perezosos cuando los encadena. Si no te aferras a la cabeza, no construyes grandes secuencias perezosas que no son necesarias, pero aún construyes esas abstracciones intermedias para cada elemento perezoso.
noisesmith

Un ejemplo sería bueno.
appshare.co

8
@LyubomyrShaydariv Por "colección intermedia", noisesmith no significa "iterar / reificar una colección completa, luego iterar / reificar otra colección completa". Quiere decir que cuando anida llamadas a funciones que devuelven secuenciales, cada llamada a función da como resultado la creación de una nueva secuencia. La iteración real todavía solo ocurre una vez, pero hay un consumo de memoria adicional y asignación de objetos debido a los secuenciales anidados.
erikprice

22

Los transductores son un medio de combinación para reducir funciones.

Ejemplo: las funciones reductoras son funciones que toman dos argumentos: un resultado hasta ahora y una entrada. Devuelven un nuevo resultado (hasta ahora). Por ejemplo+ : con dos argumentos, puede pensar en el primero como el resultado hasta ahora y en el segundo como la entrada.

Un transductor ahora podría tomar la función + y convertirla en una función doble más (duplica cada entrada antes de agregarla). Así es como se vería ese transductor (en términos más básicos):

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

Para la ilustración, sustituya rfncon +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

Entonces

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

ahora daría 12.

Las funciones reductoras devueltas por los transductores son independientes de cómo se acumula el resultado porque se acumulan con la función reductora que se les pasa, sin saber cómo. Aquí usamos en conjlugar de +. Conjtoma una colección y un valor y devuelve una nueva colección con ese valor agregado.

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

produciría [2 4 6]

También son independientes del tipo de fuente de entrada.

Se pueden encadenar varios transductores como una receta (encadenable) para transformar las funciones de reducción.

Actualización: dado que ahora hay una página oficial al respecto, recomiendo leerla: http://clojure.org/transducers


Buena explicación, pero pronto me metí en demasiada jerga: "Las funciones de reducción generadas por los transductores son independientes de cómo se acumula el resultado".
appshare.co

1
Tienes razón, la palabra generada no era apropiada aquí.
Leon Grapenthin

Está bien. De todos modos, entiendo que los Transformers son solo una optimización ahora, por lo que probablemente no deberían usarse de todos modos
appshare.co

1
Son un medio de combinación para reducir funciones. ¿Dónde más tienes eso? Esto es mucho más que una optimización.
Leon Grapenthin

Encuentro esta respuesta muy interesante, pero no tengo claro cómo se conecta a los transductores (en parte porque todavía encuentro el tema confuso). ¿Cuál es la relación entre doubley transduce?
Marte

21

Supongamos que desea utilizar una serie de funciones para transformar un flujo de datos. El shell de 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 lee de la salida de los procesos anteriores, por lo que hay cuatro flujos intermedios. Puede imaginar una implementación diferente que componga los cinco comandos en un solo comando agregado, que leería su entrada y escribiría su salida exactamente una vez. Si las corrientes intermedias fueran caras y la composición barata, eso podría ser una buena compensación.

Lo mismo ocurre con Clojure. Hay varias formas de expresar una canalización 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 facilitan la tarea. Una innovación anterior de Clojure, los reductores, también le permiten hacer eso, 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 largo o menos comprensible, y si está trabajando con muchos datos, los transductores pueden hacer su código Más rápido.

Esta es una descripción general bastante buena de los transductores.


1
Ah, entonces los transductores son principalmente una optimización del rendimiento, ¿es eso lo que estás diciendo?
appshare.co

@Zubair Sí, es cierto. Tenga en cuenta que la optimización va más allá de la eliminación de flujos intermedios; también puede realizar operaciones en paralelo.
user100464

2
Vale la pena mencionarlo pmap, que no parece recibir suficiente atención. Si está maphaciendo ping a una función costosa sobre una secuencia, hacer que la operación sea paralela es tan fácil como agregar "p". No es necesario cambiar nada más en su código, y está disponible ahora, no alfa, no beta. (Si la función crea secuencias intermedias, entonces los transductores podrían ser más rápidos, supongo.)
Marte

10

Rich Hickey dio una charla sobre 'Transducers' en la conferencia Strange Loop 2014 (45 min).

Explica de manera sencilla qué son los transductores, con ejemplos del mundo real: procesamiento de maletas en un aeropuerto. Separa claramente los diferentes aspectos y los contrasta con los enfoques actuales. Hacia el final, da la justificación de su existencia.

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


8

Descubrí que leer ejemplos de transducers-js me ayuda a entenderlos en términos concretos de cómo podría usarlos en el código del día a día.

Por ejemplo, considere este ejemplo (tomado de README en el enlace anterior):

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, el uso se xfve mucho más limpio que la alternativa habitual con Underscore.

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

¿Por qué el ejemplo de los transductores es mucho más largo? La versión de subrayado parece mucho más concisa
appshare.co

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

7

Los transductores son (¡a mi entender!) Funciones que toman una función reductora y devuelven otra. Una función reductora es aquella 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, mi transductor toma una función de filtrado de entrada que aplica a 0, entonces, ¿si ese valor es par? en el primer caso, el filtro pasa ese valor al contador, luego filtra el siguiente valor. En lugar de filtrar primero y luego pasar todos esos valores para contar.

Es lo mismo en el segundo ejemplo, verifica un valor a la vez y si ese valor es menor que 3, entonces permite contar sumar 1.


Me gustó esta simple explicación
Ignacio

7

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? Lo podemos averiguar 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 forma 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, también es muy poderoso cuando se tienen en cuenta subgrupos. Por ejemplo, si quisiéramos saber cuántos niños hay en Brown Family, 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 estos ejemplos le resulten útiles. Puedes encontrar más aquí

Espero eso ayude.

Clemencio Morales Lucas.


3
Los "transductores son una forma poderosa y componible de construir transformaciones algorítmicas que puede reutilizar en muchos contextos, y están llegando a Clojure core y core.async". ¿La definición podría aplicarse a casi cualquier cosa?
appshare.co

1
Para casi cualquier transductor Clojure, diría yo.
Clemencio Morales Lucas

6
Es más una declaración de misión que una definición.
Marte

4

Escribí en un blog sobre esto con un ejemplo de 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 tal como lo leo. Si piensa en la operación conso conjque está codificada en operaciones como map, filteretc., la función de reducción era inalcanzable.

Con los transductores, la función de reducción está desacoplada y puedo reemplazarla como lo hice con la matriz javascript nativa pushgracias 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 proporcionar su propia función de reducción.


4

Aquí está mi (en su mayoría) jerga y respuesta sin código.

Piense en los datos de dos maneras, una secuencia (valores que ocurren a lo largo del tiempo, como eventos) o una estructura (datos que existen en un momento determinado, como una lista, un vector, una matriz, etc.).

Hay determinadas operaciones que puede que desee realizar sobre corrientes o estructuras. Una de esas operaciones es el mapeo. Una función de mapeo podría incrementar cada elemento de datos (asumiendo que es un número) en 1 y es de esperar que pueda imaginar cómo esto podría aplicarse a una secuencia o estructura.

Una función de mapeo es sólo 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 los valores que coinciden con un predicado (por ejemplo, elimina todos los valores que son pares).

Los transductores le permiten "envolver" una secuencia de una o más funciones reductoras y producir un "paquete" (que en sí mismo es una función) que funciona en ambos flujos o 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 transductor en un flujo o estructura de valores (o ambos) .

Entonces, ¿qué tiene de especial esto? Por lo general, las funciones de reducción no se pueden componer de manera eficiente para trabajar tanto en flujos como en 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 algo de maquinaria adicional (es decir, el transductor) para darle esta potencia adicional.


2

Por lo que tengo entendido, son como bloques de construcción , desacoplados de la implementación de entrada y salida. Solo define la operación.

Como la implementación de la operación no está en el código de la 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 poco clara.


1

Encuentro que esta publicación le brinda una vista más panorámica del transductor.

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


3
Las respuestas que se basan simplemente en enlaces externos no se recomiendan en SO, ya que los enlaces pueden romperse en cualquier momento en el futuro. En su lugar, cite el contenido en su respuesta.
Vincent Cantin

@VincentCantin De hecho, la publicación de Medium fue eliminada.
Dmitri Zaitsev

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.