Alguém pode me explicar os transdutores Clojure em termos simples?


100

Eu tentei ler sobre isso, mas ainda não entendo o valor deles ou o que eles substituem. E eles tornam meu código mais curto, mais compreensível ou o quê?

Atualizar

Muitas pessoas postaram respostas, mas seria bom ver exemplos de com e sem transdutores para algo muito simples, que até um idiota como eu pode entender. A menos, é claro, que os transdutores precisem de um certo alto nível de compreensão, caso em que nunca irei entendê-los :(

Respostas:


75

Os transdutores são receitas do que fazer com uma sequência de dados sem saber qual é a sequência subjacente (como fazer). Pode ser qualquer canal seq, assíncrono ou talvez observável.

Eles são composíveis e polimórficos.

A vantagem é que você não precisa implementar todos os combinadores padrão sempre que uma nova fonte de dados é adicionada. De novo e de novo. Como resultado, você, como usuário, pode reutilizar essas receitas em diferentes fontes de dados.

Atualização de anúncio

Antes da versão 1.7 do Clojure, você tinha três maneiras de escrever consultas de fluxo de dados:

  1. chamadas aninhadas
    (reduce + (filter odd? (map #(+ 2 %) (range 0 10))))
  1. composição funcional
    (def xform
      (comp
        (partial filter odd?)
        (partial map #(+ 2 %))))
    (reduce + (xform (range 0 10)))
  1. macro de threading
    (defn xform [xs]
      (->> xs
           (map #(+ 2 %))
           (filter odd?)))
    (reduce + (xform (range 0 10)))

Com transdutores, você escreverá como:

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

Todos eles fazem o mesmo. A diferença é que você nunca chama os transdutores diretamente, você os passa para outra função. Os transdutores sabem o que fazer, a função que obtém o transdutor sabe como. A ordem dos combinadores é como você escreve com macro de threading (ordem natural). Agora você pode reutilizar xformcom o canal:

(chan 1 xform)

3
Eu estava mais procurando uma resposta que vem com um exemplo que me mostra como os transdutores economizam meu tempo.
appshare.co

Eles não fazem isso se você não for Clojure ou algum mantenedor da biblioteca de fluxo de dados.
Aleš Roubíček

5
Não é uma decisão técnica. Nós apenas usamos decisões baseadas no valor de negócios. "Just use-los" fará com que eu seja demitido
appshare.co

1
Você pode ter mais facilidade em manter seu trabalho se adiar a tentativa de usar os transdutores até que o Clojure 1.7 seja lançado.
user100464 de

7
Os transdutores parecem ser uma maneira útil de abstrair várias formas de objetos iteráveis. Eles podem ser não consumíveis, como Clojure seqs, ou consumíveis (como canais assíncronos). A esse respeito, parece-me que você se beneficiaria muito com o uso de transdutores se, por exemplo, alternar de uma implementação baseada em seq para uma implementação core.async usando canais. Os transdutores devem permitir que você mantenha o núcleo de sua lógica inalterado. Usando o processamento tradicional baseado em sequência, você teria que converter isso para usar transdutores ou algum analógico core-assíncrono. Esse é o caso de negócios.
Nathan Davis

47

Os transdutores melhoram a eficiência e permitem que você escreva um código eficiente de uma forma mais modular.

Este é um ensaio decente .

Comparado a compor as chamadas para o antigo map, filter, reduceetc. você obter um melhor desempenho, porque você não precisa construir coleções intermediárias entre cada passo, e repetidamente andar essas coleções.

Comparado a reducers, ou compondo manualmente todas as suas operações em uma única expressão, você fica mais fácil de usar abstrações, melhor modularidade e reutilização de funções de processamento.


2
Só por curiosidade, você disse acima: "construir coleções intermediárias entre cada etapa". Mas as "coleções intermediárias" não soam como um antipadrão? .NET oferece enumeráveis ​​preguiçosos, Java oferece streams preguiçosos ou iteráveis ​​orientados pelo Guava, Haskell preguiçoso deve ter algo preguiçoso também. Nenhum deles requer map/ reducepara usar coleções intermediárias porque todos eles constroem uma cadeia iteradora. Onde estou errado aqui?
Lyubomyr Shaydariv

3
Clojure mape filtercrie coleções intermediárias quando aninhadas.
noisesmith

4
E, pelo menos em relação à versão de preguiça de Clojure, a questão da preguiça é ortogonal aqui. Sim, o mapa e o filtro são preguiçosos, e também geram contêineres para valores preguiçosos quando você os encadeia. Se você não se apegar à cabeça, não construirá grandes sequências preguiçosas desnecessárias, mas ainda assim construirá aquelas abstrações intermediárias para cada elemento preguiçoso.
noisesmith

Um exemplo seria bom.
appshare.co

8
@LyubomyrShaydariv Por "coleção intermediária", noisesmith não significa "iterar / reificar uma coleção inteira, depois iterar / reificar outra coleção inteira". Ele quer dizer que, quando você aninha chamadas de função que retornam sequenciais, cada chamada de função resulta na criação de uma nova sequência. A iteração real ainda acontece apenas uma vez, mas há consumo de memória adicional e alocação de objetos devido às sequenciais aninhadas.
erikprice

22

Os transdutores são um meio de combinação para funções de redução.

Exemplo: funções de redução são funções que aceitam dois argumentos: um resultado até agora e uma entrada. Eles retornam um novo resultado (até agora). Por exemplo +: Com dois argumentos, você pode pensar no primeiro como o resultado até agora e no segundo como a entrada.

Um transdutor agora pode pegar a função + e torná-la uma função duas vezes mais (duplica cada entrada antes de adicioná-la). É assim que esse transdutor seria (em termos mais básicos):

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

Para ilustração, substitua rfnpor +para ver como +é transformado em duas vezes mais:

(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

assim

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

agora renderia 12.

As funções de redução retornadas pelos transdutores são independentes de como o resultado é acumulado porque elas se acumulam com a função de redução passada a eles, sem saber como. Aqui usamos em conjvez de +. Conjpega uma coleção e um valor e retorna uma nova coleção com esse valor anexado.

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

renderia [2 4 6]

Eles também são independentes do tipo de fonte da entrada.

Vários transdutores podem ser encadeados como uma receita (encadeada) para transformar funções de redução.

Atualização: Como agora existe uma página oficial sobre o assunto, recomendo fortemente que a leia: http://clojure.org/transducers


Boa explicação, mas logo entrou em muitos jargões para mim, "Reduzir funções geradas por transdutores são independentes de como o resultado é acumulado".
appshare.co

1
Você está certo, a palavra gerada era inadequada aqui.
Leon Grapenthin

Está certo. De qualquer forma, eu entendo que os Transformers são apenas uma otimização agora, então provavelmente não deveriam ser usados ​​de qualquer maneira
appshare.co

1
Eles são um meio de combinação para reduzir funções. Onde mais você tem isso? Isso é muito mais do que uma otimização.
Leon Grapenthin

Acho essa resposta muito interessante, mas não está claro para mim como ela se conecta aos transdutores (em parte porque ainda acho o assunto confuso). Qual é a relação entre doublee transduce?
Marte

21

Digamos que você queira usar uma série de funções para transformar um fluxo de dados. O shell Unix permite que você faça esse tipo de coisa com o operador pipe, por exemplo

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

(O comando acima conta o número de usuários com a letra r maiúscula ou minúscula em seu nome de usuário). Isso é implementado como um conjunto de processos, cada um dos quais lê a saída dos processos anteriores, portanto, há quatro fluxos intermediários. Você pode imaginar uma implementação diferente que compõe os cinco comandos em um único comando agregado, que lê sua entrada e grava sua saída exatamente uma vez. Se os fluxos intermediários fossem caros e a composição barata, isso poderia ser uma boa troca.

O mesmo tipo de coisa vale para Clojure. Existem várias maneiras de expressar um pipeline de transformações, mas dependendo de como você faz isso, você pode acabar com fluxos intermediários passando de uma função para a próxima. Se você tiver muitos dados, é mais rápido compor essas funções em uma única função. Os transdutores facilitam fazer isso. Uma inovação anterior do Clojure, os redutores, permitem que você faça isso também, mas com algumas restrições. Os transdutores removem algumas dessas restrições.

Então, para responder à sua pergunta, os transdutores não necessariamente tornarão seu código mais curto ou mais compreensível, mas seu código provavelmente não será mais longo ou menos compreensível também, e se você estiver trabalhando com muitos dados, os transdutores podem tornar seu código Mais rápido.

Esta é uma visão geral muito boa dos transdutores.


1
Ah, então os transdutores são principalmente uma otimização de desempenho, é isso que você está dizendo?
appshare.co

@Zubair Sim, isso mesmo. Observe que a otimização vai além da eliminação de fluxos intermediários; você também pode realizar operações em paralelo.
user100464

2
Vale a pena mencionar pmap, o que parece não chamar atenção suficiente. Se você estiver mapexecutando o ping de uma função cara em uma sequência, tornar a operação paralela é tão fácil quanto adicionar "p". Não há necessidade de alterar mais nada em seu código, e ele está disponível agora - nem alfa, nem beta. (Se a função cria sequências intermediárias, então os transdutores podem ser mais rápidos, eu acho.)
Marte

10

Rich Hickey deu uma palestra sobre 'Transdutores' na conferência Strange Loop 2014 (45 min).

Ele explica de forma simples o que são transdutores, com exemplos do mundo real - processamento de malas em um aeroporto. Ele separa claramente os diferentes aspectos e os contrasta com as abordagens atuais. Perto do fim, ele dá a justificativa de sua existência.

Vídeo: https://www.youtube.com/watch?v=6mTbuzafcII


8

Descobri que ler exemplos de transdutores-js me ajuda a entendê-los em termos concretos de como posso usá-los no código do dia-a-dia.

Por exemplo, considere este exemplo (retirado do README no link acima):

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 um lado, usar xfparece muito mais limpo do que a alternativa usual com Underscore.

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

Por que o exemplo dos transdutores é muito mais longo. A versão sublinhada parece muito mais concisa
appshare.co

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

7

Transdutores são (no meu entendimento!) Funções que assumem uma função de redução e retornam outra. Uma função redutora é aquela que

Por exemplo:

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

Neste caso, meu-transdutor assume uma função de filtragem de entrada que se aplica a 0, então se esse valor for par? no primeiro caso, o filtro passa esse valor para o contador e, em seguida, filtra o próximo valor. Em vez de primeiro filtrar e, em seguida, passar todos esses valores para a contagem.

É a mesma coisa no segundo exemplo, ele verifica um valor por vez e se esse valor for menor que 3, então ele permite contar somar 1.


Gostei dessa explicação simples
Ignacio

7

Uma definição clara do transdutor está aqui:

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 entender isso, vamos considerar o seguinte exemplo simples:

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

Que tal, queremos saber quantas crianças há na aldeia? Podemos descobrir facilmente com o seguinte redutor:

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

Aqui está outra maneira de fazer isso:

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

Além disso, é muito poderoso quando se leva em conta subgrupos. Por exemplo, se quisermos saber quantas crianças há na Família Marrom, podemos executar:

;; 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 esses exemplos sejam úteis. Você pode encontrar mais aqui

Espero que ajude.

Clemencio Morales Lucas.


3
Os "transdutores são uma forma poderosa e combinável de construir transformações algorítmicas que você pode reutilizar em muitos contextos, e eles estão chegando ao Clojure core e core.async." definição poderia se aplicar a quase tudo?
appshare.co

1
Para quase qualquer transdutor Clojure, eu diria.
Clemencio Morales Lucas

6
É mais uma declaração de missão do que uma definição.
Marte,

4

Eu bloguei sobre isso com um exemplo de clojurescript que explica como as funções de sequência agora são extensíveis ao serem capazes de substituir a função de redução.

Este é o ponto dos transdutores conforme eu leio. Se você pensar sobre a operação consou conjque está codificada em operações como map, filteretc., a função de redução estava inacessível.

Com transdutores, a função de redução é desacoplada e posso substituí-la como fiz com o array nativo de javascript, pushgraças aos transdutores.

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

filter e amigos têm uma nova operação 1 aridade que retornará uma função de transdução que você pode usar para fornecer sua própria função de redução.


4

Aqui está meu jargão (principalmente) e resposta livre de códigos.

Pense nos dados de duas maneiras, um fluxo (valores que ocorrem ao longo do tempo, como eventos) ou uma estrutura (dados que existem em um ponto no tempo, como uma lista, um vetor, uma matriz, etc.).

Existem certas operações que você pode querer realizar sobre fluxos ou estruturas. Uma dessas operações é o mapeamento. Uma função de mapeamento pode incrementar cada item de dados (assumindo que seja um número) em 1 e você pode imaginar como isso poderia se aplicar a um fluxo ou estrutura.

Uma função de mapeamento é apenas uma de uma classe de funções que às vezes são chamadas de "funções de redução". Outra função de redução comum é o filtro que remove valores que correspondem a um predicado (por exemplo, remove todos os valores que são pares).

Os transdutores permitem "embrulhar" uma sequência de uma ou mais funções redutoras e produzir um "pacote" (que é uma função) que funciona em ambos os fluxos ou estruturas. Por exemplo, você pode "empacotar" uma sequência de funções de redução (por exemplo, filtrar números pares, em seguida, mapear os números resultantes para incrementá-los em 1) e, em seguida, usar esse "pacote" de transdutor em um fluxo ou estrutura de valores (ou ambos) .

Então, o que há de especial nisso? Normalmente, funções de redução não podem ser compostas de forma eficiente para funcionar em fluxos e estruturas.

Portanto, o benefício para você é que você pode alavancar seu conhecimento sobre essas funções e aplicá-las a mais casos de uso. O custo para você é que você precisa aprender algumas máquinas extras (ou seja, o transdutor) para fornecer essa potência extra.


2

Até onde eu entendo, eles são como blocos de construção , separados da implementação de entrada e saída. Você apenas define a operação.

Como a implementação da operação não está no código de entrada e nada é feito na saída, os transdutores são extremamente reutilizáveis. Eles me lembram de Flow em Akka Streams .

Eu também sou novo em transdutores, desculpe pela resposta possivelmente obscura.


1

Acho que este post dá a vocês uma visão mais panorâmica do transdutor.

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


3
Respostas baseadas apenas em links externos são desencorajadas no SO, pois os links podem quebrar a qualquer momento no futuro. Em vez disso, cite o conteúdo da sua resposta.
Vincent Cantin,

@VincentCantin Na verdade, a postagem no Medium foi excluída.
Dmitri Zaitsev

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.