Czy ktoś może mi wyjaśnić prostymi słowami przetworniki Clojure?


100

Próbowałem to przeczytać, ale nadal nie rozumiem ich wartości ani tego, co zastępują. I czy sprawiają, że mój kod jest krótszy, bardziej zrozumiały, czy co?

Aktualizacja

Wiele osób zamieściło odpowiedzi, ale byłoby miło zobaczyć przykłady z przetwornikami i bez nich dla czegoś bardzo prostego, co nawet taki idiota jak ja może zrozumieć. O ile oczywiście przetworniki nie wymagają pewnego wysokiego poziomu zrozumienia, w takim przypadku nigdy ich nie zrozumiem :(

Odpowiedzi:


75

Przetworniki to przepisy na to, co zrobić z sekwencją danych bez wiedzy, jaka jest podstawowa sekwencja (jak to zrobić). Może to być dowolny kanał sekwencyjny, asynchroniczny lub obserwowalny.

Są kompozycyjne i polimorficzne.

Zaletą jest to, że nie musisz implementować wszystkich standardowych kombinatorów za każdym razem, gdy dodawane jest nowe źródło danych. Znowu i znowu. W rezultacie jako użytkownik możesz ponownie wykorzystać te receptury w różnych źródłach danych.

Aktualizacja reklamy

We wcześniejszej wersji 1.7 Clojure istniały trzy sposoby pisania zapytań przepływu danych:

  1. połączenia zagnieżdżone
    (reduce + (filter odd? (map #(+ 2 %) (range 0 10))))
  1. skład funkcjonalny
    (def xform
      (comp
        (partial filter odd?)
        (partial map #(+ 2 %))))
    (reduce + (xform (range 0 10)))
  1. makro gwintowania
    (defn xform [xs]
      (->> xs
           (map #(+ 2 %))
           (filter odd?)))
    (reduce + (xform (range 0 10)))

Przy przetwornikach napiszesz to tak:

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

Wszyscy robią to samo. Różnica polega na tym, że nigdy nie wywołujesz bezpośrednio przetworników, tylko przekazujesz je do innej funkcji. Przetworniki wiedzą, co robić, funkcja, którą otrzymuje przetwornik, wie jak. Kolejność kombinatorów jest taka, jak w przypadku makra gwintowania (kolejność naturalna). Teraz możesz ponownie wykorzystać xformkanał:

(chan 1 xform)

3
Bardziej szukałem odpowiedzi, która zawiera przykład, który pokazuje, jak przetworniki oszczędzają czas.
appshare.co

Nie robią tego, jeśli nie jesteś Clojure lub jakimś opiekunem biblioteki Dataflow.
Aleš Roubíček

5
To nie jest decyzja techniczna. Podejmujemy się wyłącznie decyzji opartych na wartości biznesowej. „Po prostu ich użyj” spowoduje zwolnienie mnie
appshare.co

1
Utrzymanie pracy może być łatwiejsze, jeśli opóźnisz próbę użycia głowic do czasu wydania Clojure 1.7.
user100464

7
Wydaje się, że przetworniki są użytecznym sposobem abstrahowania nad różnymi formami iterowalnych obiektów. Mogą to być nieużywalne, takie jak sekwencje Clojure lub zużywalne (takie jak kanały asynchroniczne). W związku z tym wydaje mi się, że korzystanie z przetworników przyniosłoby ogromne korzyści, gdyby np. Przełączył się z implementacji opartej na sekwencjach na implementację core.async przy użyciu kanałów. Przetworniki powinny pozwolić ci zachować rdzeń twojej logiki niezmieniony. Używając tradycyjnego przetwarzania opartego na sekwencjach, musiałbyś przekonwertować to, aby użyć przetworników lub jakiegoś analogowego rdzenia asynchronicznego. To jest uzasadnienie biznesowe.
Nathan Davis

47

Przetworniki zwiększają wydajność i umożliwiają pisanie wydajnego kodu w bardziej modułowy sposób.

To przyzwoity przebieg .

W porównaniu do tworzenia połączeń do starego map, filter, reduceitd. Można uzyskać lepszą wydajność, ponieważ nie trzeba budować kolekcje pośrednich pomiędzy każdym kroku, a wielokrotnie chodzić te zbiory.

W porównaniu z reducersręcznym komponowaniem wszystkich operacji w jedno wyrażenie, uzyskujesz łatwiejsze w użyciu abstrakcje, lepszą modułowość i ponowne wykorzystanie funkcji przetwarzania.


2
Zaciekawiony, powiedziałeś powyżej: „budować kolekcje pośrednie między każdym krokiem”. Ale czy „kolekcje pośrednie” nie brzmią jak anty-wzór? .NET oferuje leniwe wyliczenia, Java oferuje leniwe strumienie lub iterowalne oparte na guawie, leniwy Haskell też musi mieć coś leniwego. Żadne z nich nie wymaga map/ reducenie używa kolekcji pośrednich, ponieważ wszystkie z nich tworzą łańcuch iteratorów. Gdzie się mylę?
Lyubomyr Shaydariv,

3
Klonuj mapi filtertwórz kolekcje pośrednie po zagnieżdżeniu.
noisesmith

4
A przynajmniej jeśli chodzi o wersję lenistwa Clojure, kwestia lenistwa jest tutaj ortogonalna. Tak, mapowanie i filtr są leniwe, generują również kontenery dla leniwych wartości podczas ich łączenia. Jeśli nie trzymasz się głowy, nie tworzysz dużych leniwych sekwencji, które nie są potrzebne, ale nadal tworzysz te pośrednie abstrakcje dla każdego leniwego elementu.
noisesmith

Przykład byłby miły.
appshare.co

8
@LyubomyrShaydariv Przez „kolekcję pośrednią”, noisesmith nie oznacza „iteruj / reifikuj całą kolekcję, a następnie iteruj / reifikuj kolejną całą kolekcję”. Oznacza to, że kiedy zagnieżdżasz wywołania funkcji, które zwracają sekwencyjne, każde wywołanie funkcji powoduje utworzenie nowej sekwencji. Rzeczywista iteracja nadal występuje tylko raz, ale występuje dodatkowe zużycie pamięci i alokacja obiektów z powodu zagnieżdżonych sekwencji.
erikprice

22

Przetworniki są środkiem łączącym w celu ograniczenia funkcji.

Przykład: Funkcje redukujące to funkcje, które przyjmują dwa argumenty: dotychczasowy wynik i dane wejściowe. Zwracają nowy wynik (jak dotąd). Na przykład +: Mając dwa argumenty, możesz traktować pierwszy jako wynik, a drugi jako dane wejściowe.

Przetwornik może teraz przejąć funkcję + i uczynić ją funkcją podwójnego plus (podwaja każde wejście przed dodaniem). Tak wyglądałby ten przetwornik (w większości podstawowych pojęć):

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

Dla ilustracji substytut rfnz +zobaczyć, jak +przekształca się dwa razy-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

Więc

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

dałoby teraz 12.

Funkcje redukcyjne zwracane przez przetworniki są niezależne od tego, w jaki sposób kumulowany jest wynik, ponieważ gromadzą się one wraz z przekazaną im funkcją redukcyjną, nieświadomie w jaki sposób. Tutaj używamy conjzamiast +. Conjpobiera kolekcję i wartość i zwraca nową kolekcję z dołączoną wartością.

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

dałoby [2 4 6]

Są również niezależne od rodzaju źródła danych wejściowych.

Wiele przetworników można łączyć w łańcuch (dający się połączyć w łańcuch) przepis na przekształcenie funkcji redukujących.

Aktualizacja: Ponieważ istnieje już oficjalna strona o tym, gorąco polecam ją przeczytać: http://clojure.org/transducers


Niezłe wyjaśnienie, ale wkrótce doszedłem do zbyt dużego żargonu dla mnie: „Funkcje redukcyjne generowane przez przetworniki są niezależne od tego, jak kumuluje się wynik”.
appshare.co

1
Masz rację, wygenerowane słowo było tutaj nieodpowiednie.
Leon Grapenthin

W porządku. W każdym razie rozumiem, że Transformers są teraz tylko optymalizacją, więc prawdopodobnie i tak nie powinny być używane
appshare.co

1
Są środkiem łączącym redukcję funkcji. Gdzie jeszcze to masz? To znacznie więcej niż optymalizacja.
Leon Grapenthin

Uważam, że ta odpowiedź jest bardzo interesująca, ale nie jest dla mnie jasne, w jaki sposób łączy się z przetwornikami (częściowo dlatego, że nadal uważam ten temat za zagmatwany). Jaki jest związek między doublei transduce?
Mars

21

Załóżmy, że chcesz użyć szeregu funkcji do przekształcenia strumienia danych. Powłoka systemu Unix pozwala robić tego typu rzeczy za pomocą operatora potoku, np

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

(Powyższe polecenie zlicza użytkowników, których nazwa użytkownika zawiera literę r wielką lub małą). Jest to realizowane jako zestaw procesów, z których każdy odczytuje dane wyjściowe z poprzednich procesów, więc istnieją cztery strumienie pośrednie. Można sobie wyobrazić inną implementację, która łączy pięć poleceń w jedno zagregowane polecenie, które odczytuje dane wejściowe i zapisuje dane wyjściowe dokładnie raz. Gdyby strumienie pośrednie były drogie, a skład tani, może to być dobry kompromis.

To samo dotyczy Clojure. Istnieje wiele sposobów wyrażenia potoku transformacji, ale w zależności od tego, jak to zrobisz, możesz skończyć z pośrednimi strumieniami przechodzącymi od jednej funkcji do drugiej. Jeśli masz dużo danych, szybsze będzie połączenie tych funkcji w jedną funkcję. Przetworniki ułatwiają to. Wcześniejsza innowacja Clojure, reduktory, również na to pozwalają, ale z pewnymi ograniczeniami. Przetworniki usuwają niektóre z tych ograniczeń.

Więc odpowiadając na twoje pytanie, przetworniki niekoniecznie spowodują, że twój kod będzie krótszy lub bardziej zrozumiały, ale twój kod prawdopodobnie też nie będzie dłuższy lub mniej zrozumiały, a jeśli pracujesz z dużą ilością danych, przetworniki mogą sprawić, że twój kod szybciej.

To całkiem niezły przegląd przetworników.


1
Ach, więc przetworniki to głównie optymalizacja wydajności, czy to właśnie mówisz?
appshare.co

@Zubair Tak, zgadza się. Należy pamiętać, że optymalizacja wykracza poza eliminację strumieni pośrednich; możesz również wykonywać operacje równolegle.
user100464

2
Warto o tym wspomnieć pmap, co nie wydaje się przyciągać wystarczającej uwagi. Jeśli mappingujesz kosztowną funkcję w sekwencji, równoległe wykonanie operacji jest tak proste, jak dodanie „p”. Nie musisz niczego zmieniać w swoim kodzie i jest już dostępny - nie alfa, ani beta. (Jeśli funkcja tworzy sekwencje pośrednie, to chyba przetworniki mogą być szybsze.)
Mars

10

Rich Hickey wygłosił wykład „Transducers” na konferencji Strange Loop 2014 (45 min).

Wyjaśnia w prosty sposób, czym są przetworniki, na przykładach z prawdziwego świata - przetwarzanie worków na lotnisku. Wyraźnie rozdziela różne aspekty i zestawia je z obecnymi podejściami. Pod koniec podaje uzasadnienie ich istnienia.

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


8

Odkryłem, że czytanie przykładów z transducers-js pomaga mi zrozumieć je w konkretny sposób, w jaki sposób mogę ich używać w codziennym kodzie.

Na przykład rozważ ten przykład (zaczerpnięty z pliku README pod powyższym linkiem):

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]

Po pierwsze, użycie xfwygląda o wiele bardziej czysto niż zwykła alternatywa dla Underscore.

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

Dlaczego przykład przetworników jest o wiele dłuższy. Wersja podkreślona wygląda o wiele bardziej zwięźle
appshare.co

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

7

Przetworniki to (w moim rozumieniu!) Funkcje, które przejmują jedną funkcję redukcyjną, a zwracają inną. Funkcja redukująca to taka, która

Na przykład:

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

W tym przypadku mój-przetwornik przyjmuje funkcję filtrowania sygnału wejściowego, którą stosuje do 0, to jeśli ta wartość jest parzysta? w pierwszym przypadku filtr przekazuje tę wartość do licznika, a następnie filtruje następną wartość. Zamiast najpierw filtrować, a następnie przekazywać wszystkie te wartości do licznika.

Tak samo jest w drugim przykładzie, który sprawdza po jednej wartości na raz i jeśli ta wartość jest mniejsza niż 3, to pozwala policzyć dodać 1.


Podobało mi się to proste wyjaśnienie
Ignacio

7

Oto wyraźna definicja przetwornika:

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.

Aby to zrozumieć, rozważmy następujący prosty przykład:

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

A co z tym, że chcemy wiedzieć, ile dzieci jest we wsi? Z łatwością przekonamy się o tym przy pomocy następującego reduktora:

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

Oto inny sposób, aby to zrobić:

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

Poza tym jest naprawdę potężny, jeśli wziąć pod uwagę podgrupy. Na przykład, jeśli chcielibyśmy wiedzieć, ile dzieci jest w rodzinie Brown, możemy wykonać:

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

Mam nadzieję, że te przykłady okażą się pomocne. Więcej znajdziesz tutaj

Mam nadzieję, że to pomoże.

Clemencio Morales Lucas.


3
„Przetworniki to potężny i dający się skomponować sposób tworzenia algorytmicznych przekształceń, które można ponownie wykorzystać w wielu kontekstach, i trafiają do rdzeni Clojure i core.async”. definicja może dotyczyć prawie wszystkiego?
appshare.co

1
Powiedziałbym, że do prawie każdego przetwornika Clojure.
Clemencio Morales Lucas

6
To bardziej deklaracja misji niż definicja.
Mars

4

Napisałem o tym na blogu z przykładem skryptu clojurescript, który wyjaśnia, w jaki sposób funkcje sekwencji są teraz rozszerzalne dzięki możliwości zastąpienia funkcji redukującej.

Taki jest sens przetworników, gdy to czytam. Jeśli myślisz o conslub conjoperacji, która jest zakodowana w operacjach takich map, filteritd., Funkcja redukcji był nieosiągalny.

W przypadku przetworników funkcja redukcji jest odsprzęgnięta i mogę ją zastąpić, tak jak zrobiłem z natywną tablicą javascript pushdzięki przetwornikom.

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

filter i przyjaciele mają nową operację 1-arową, która zwróci funkcję przetwornika, której możesz użyć do dostarczenia własnej funkcji redukcji.


4

Oto mój (głównie) żargon i odpowiedź bez kodu.

Pomyśl o danych w dwojaki sposób: o strumieniu (wartościach, które pojawiają się w czasie, takie jak zdarzenia) lub strukturze (danych, które istnieją w określonym momencie, takich jak lista, wektor, tablica itp.).

Istnieją pewne operacje, które możesz chcieć wykonać na strumieniach lub strukturach. Jedną z takich operacji jest mapowanie. Funkcja mapowania może zwiększyć każdy element danych (zakładając, że jest to liczba) o 1 i miejmy nadzieję, że można sobie wyobrazić, jak można to zastosować do strumienia lub struktury.

Funkcja odwzorowująca to tylko jedna z klas funkcji, które są czasami nazywane „funkcjami redukcyjnymi”. Inną popularną funkcją redukującą jest filtr, który usuwa wartości pasujące do predykatu (np. Usuwa wszystkie wartości, które są parzyste).

Przetworniki umożliwiają „zawinięcie” sekwencji jednej lub większej liczby funkcji redukujących i utworzenie „pakietu” (który sam jest funkcją), który działa na obu strumieniach lub strukturach. Na przykład można „spakować” sekwencję funkcji redukcyjnych (np. Filtrować liczby parzyste, a następnie mapować otrzymane liczby, aby zwiększyć je o 1), a następnie użyć tego „pakietu” przetwornika w strumieniu lub strukturze wartości (lub obu) .

Więc co jest w tym specjalnego? Zazwyczaj funkcje redukcyjne nie mogą być efektywnie skomponowane do pracy zarówno na strumieniach, jak i strukturach.

Zatem korzyścią dla Ciebie jest to, że możesz wykorzystać swoją wiedzę na temat tych funkcji i zastosować je w większej liczbie przypadków użycia. Koszt jest taki, że musisz nauczyć się dodatkowej maszyny (np. Przetwornika), aby uzyskać tę dodatkową moc.


2

O ile rozumiem, są one jak bloki konstrukcyjne , oddzielone od implementacji wejścia i wyjścia. Po prostu określ operację.

Ponieważ implementacja operacji nie znajduje się w kodzie wejścia i nic nie jest wykonywane z wyjściem, przetworniki są niezwykle wielokrotnego użytku. Przypominają mi Flows w Akka Streams .

Jestem też nowy w przetwornikach, przepraszam za prawdopodobnie niejasną odpowiedź.


1

Uważam, że ten post daje bardziej widok z lotu ptaka na przetwornik.

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


3
Odpowiedzi opierające się wyłącznie na linkach zewnętrznych są odradzane w przypadku SO, ponieważ linki mogą zostać przerwane w dowolnym momencie w przyszłości. Zamiast tego zacytuj treść swojej odpowiedzi.
Vincent Cantin,

@VincentCantin W rzeczywistości średni post został usunięty.
Dmitri Zaitsev

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.