Чи може хтось пояснити мені перетворювачі Clojure простими словами?


100

Я спробував прочитати з цього приводу, але все ще не розумію значення їх чи того, що вони замінюють. І чи роблять мій код коротшим, зрозумілішим чи що?

Оновлення

Багато людей розмістили відповіді, але було б непогано бачити приклади з і без перетворювачів чогось дуже простого, що навіть ідіот, як я, може зрозуміти. Якщо, звичайно, перетворювачі не потребують певного високого рівня розуміння, в такому випадку я їх ніколи не зрозумію :(

Відповіді:


75

Перетворювачі - це рецепти, що робити з послідовністю даних без знання, що є базовою послідовністю (як це зробити). Це може бути будь-яка послідовність, асинхронний канал або, можливо, спостерігається.

Вони є композиційними та поліморфними.

Перевага полягає в тому, що вам не доведеться впроваджувати всі стандартні комбінатори щоразу, коли додається нове джерело даних. Знову і знову. Як результат, ви як користувач можете повторно використовувати ці рецепти в різних джерелах даних.

Оновлення реклами

У попередній версії 1.7 Clojure у вас було три способи написання запитів на потік даних:

  1. вкладені дзвінки
    (reduce + (filter odd? (map #(+ 2 %) (range 0 10))))
  1. функціональний склад
    (def xform
      (comp
        (partial filter odd?)
        (partial map #(+ 2 %))))
    (reduce + (xform (range 0 10)))
  1. нанизування макросу
    (defn xform [xs]
      (->> xs
           (map #(+ 2 %))
           (filter odd?)))
    (reduce + (xform (range 0 10)))

З перетворювачами ви напишете так:

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

Всі вони роблять те саме. Різниця полягає в тому, що ти ніколи не дзвониш перетворювачам безпосередньо, передаєш їх іншій функції. Перетворювачі знають, що робити, функція, яка отримує перетворювач, знає, як. Порядок комбінаторів такий, як ви пишете його з макросом нарізки (природний порядок). Тепер ви можете повторно використовувати xformканал:

(chan 1 xform)

3
Я більше шукав відповіді, яка наводить приклад, який показує мені, як перетворювачі економить мені час.
appshare.co

Вони не ставляться, якщо ви не Clojure або якийсь посібник підтримки потоку даних.
Алеш Рубічек

5
Це не технічне рішення. Ми використовуємо лише рішення, засновані на вартості бізнесу. "Просто використовуйте їх" звільнять мене
appshare.co

1
Можливо, вам буде легше зберігати роботу, якщо ви затягнете спробу використання перетворювачів до виходу Clojure 1.7.
користувач100464

7
Перетворювачі здаються корисним способом абстрагування різних форм ітерабельних об'єктів. Вони можуть бути неспоживними, такими як послідовності Clojure, або споживаними (наприклад, асинхронні канали). У цьому відношенні, мені здається, ви отримаєте велику користь від використання перетворювачів, якщо, наприклад, перейдете з реалізації, заснованої на послідовності, на реалізацію core.async за допомогою каналів. Перетворювачі повинні дозволяти вам зберігати ядро ​​вашої логіки незмінним. Використовуючи традиційну обробку на основі послідовностей, вам доведеться перетворити це, щоб використовувати або перетворювачі, або якийсь аналог ядра-асинк. Оце ділова справа.
Натан Девіс

47

Перетворювачі підвищують ефективність і дозволяють писати ефективний код більш модульним способом.

Це гідний пробіг .

За порівнянні з складали виклики до старого map, filter, і reduceт.д. Ви отримуєте вищу продуктивність , так як вам не потрібно , щоб побудувати проміжні колекції кожного кроку, і кілька разів ходити ці колекції.

Порівняно з reducersабо вручну склавши всі ваші операції в один вираз, ви легше використовувати абстракції, кращу модульність і повторне використання функцій обробки.


2
Просто цікаво, ви сказали вище: "будувати проміжні колекції між кожним кроком". Але хіба "проміжні колекції" не звучать як анти-візерунок? .NET пропонує ліниві перелічувачі, Java пропонує ліниві потоки або ітерабелі, керовані гуавою, лінивий Haskell теж повинен мати щось ліниве. Жоден із них не вимагає map/ reduceвикористовувати проміжні колекції, оскільки всі вони будують ітераторний ланцюг. Де я тут помиляюся?
Любомир Шайдарів

3
Замініть mapі filterстворіть проміжні колекції, коли вони вкладені.
шумець

4
І принаймні щодо версії Клуджура про лінь тут питання про лінь є ортогональним. Так, карта і фільтр ліниві, також генерують контейнери для лінивих значень, коли ви їх ланцюгаєте. Якщо ви не тримаєтесь за голову, ви не будуєте великих лінивих послідовностей, які не потрібні, але ви все одно будуєте ці проміжні абстракції для кожного ледачого елемента.
шумець

Приклад міг би бути приємним.
appshare.co

8
@LyubomyrShaydariv Під "проміжною колекцією" noisemith не означає "повторити / повторити цілу колекцію, а потім повторити / повторити іншу цілу колекцію". Він або вона означає, що при введенні викликів функції, які повертають послідовності, кожен виклик функції призводить до створення нового послідовного. Фактична ітерація все ще відбувається лише один раз, але є додаткове споживання пам’яті та розподіл об’єктів за рахунок вкладених послідовностей.
erikprice

22

Перетворювачі - це засіб комбінації для зниження функцій.

Приклад: Редукційні функції - це функції, які беруть два аргументи: Результат поки що і вхід. Вони повертають новий результат (поки що). Наприклад +: За допомогою двох аргументів ви можете вважати перший результат як результат, а другий як вхідний.

Тепер перетворювач може взяти функцію + і зробити її функцією в два рази більше (подвоює кожен вхід перед додаванням). Ось як би виглядав цей перетворювач (у більшості основних термінів):

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

Для заміни ілюстрації rfnз , +щоб побачити , як +перетворюється в два рази плюс:

(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

Так

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

зараз дасть 12.

Функції зменшення, повернені перетворювачами, не залежать від того, як накопичується результат, оскільки вони накопичуються за допомогою функції передачі, переданої їм, невідомо як. Тут ми використовуємо conjзамість +. Conjбере колекцію та значення та повертає нову колекцію із доданим значенням.

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

вийде [2 4 6]

Вони також не залежать від того, яке джерело є джерелом.

Кілька перетворювачів можуть бути пов'язані ланцюжком як (можливий) рецепт для перетворення функцій скорочення.

Оновлення: Оскільки зараз існує офіційна сторінка про це, я настійно рекомендую ознайомитися з нею: http://clojure.org/transducers


Приємне пояснення, але незабаром потрапив у занадто багато для мене жаргону: "Зменшення функцій, що створюються перетворювачами, не залежить від того, як накопичується результат".
appshare.co

1
Ви маєте рацію, слово, що генерується, було тут недоречним.
Леон Грейпентін

Нічого страшного. У будь-якому випадку я розумію, що Трансформери - це лише оптимізація, тому, ймовірно, не слід їх використовувати
appshare.co

1
Вони є засобом комбінації для зниження функцій. Де ще це у вас є? Це набагато більше, ніж оптимізація.
Леон Грейпентін

Я вважаю цю відповідь дуже цікавою, але мені незрозуміло, як вона підключається до перетворювачів (почасти тому, що я все ще вважаю тему заплутаною). Який взаємозв'язок між doubleі transduce?
Марс

21

Скажімо, ви хочете використовувати низку функцій для перетворення потоку даних. Оболонка Unix дозволяє вам робити такі речі з оператором труб, наприклад

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

(Вищенаведена команда підраховує кількість користувачів, які мають літеру r у верхньому або нижньому регістрі у своєму імені користувача). Це реалізовано у вигляді набору процесів, кожен з яких зчитується з результатів попередніх процесів, тому є чотири проміжні потоки. Ви можете уявити собі іншу реалізацію, яка складається з п'яти команд в єдину сукупну команду, яка буде читати з її вводу і записувати свій результат точно один раз. Якщо проміжні потоки будуть дорогими, а склад - дешевим, це може бути корисним компромісом.

Така ж річ стосується і Clojure. Існує кілька способів виразити конвеєр перетворень, але залежно від того, як ви це зробите, ви можете закінчити проміжні потоки, що переходять від однієї функції до іншої. Якщо у вас багато даних, швидше скласти ці функції в одну функцію. Перетворювачі полегшують це. Раніше нововведення Clojure, редуктори, дозволяють вам це робити, але з деякими обмеженнями. Перетворювачі знімають деякі з цих обмежень.

Отже, щоб відповісти на ваше запитання, перетворювачі не обов'язково роблять ваш код коротшим або більш зрозумілим, але ваш код, ймовірно, не буде довшим або менш зрозумілим, і якщо ви працюєте з великою кількістю даних, перетворювачі можуть зробити ваш код швидше.

Це досить хороший огляд перетворювачів.


1
Так, перетворювачі - це в основному оптимізація продуктивності, це те, що ви говорите?
appshare.co

@Zubair Так, саме так. Зауважте, що оптимізація виходить за межі усунення проміжних потоків; Ви також можете паралельно виконувати операції.
користувач100464

2
Варто згадати pmap, що, схоже, не приділяє достатньої уваги. Якщо ви mapзаписуєте дорогу функцію за послідовністю, зробити операцію паралельною настільки ж просто, як і додавання "p". Не потрібно нічого змінювати у своєму коді, і це доступно зараз - не альфа, не бета-версія. (Якщо функція створює проміжні послідовності, то, можу здогадатися, перетворювачі можуть бути швидшими.)
Марс,

10

Річ Хікі виступив з доповіддю "Перетворювачі" на конференції "Дивна петля 2014" (45 хв).

Він просто пояснює, що таке перетворювачі, з прикладами реального світу - обробкою мішків в аеропорту. Він чітко розмежовує різні аспекти і протиставляє їх сучасним підходам. До кінця він дає обґрунтування їх існуванню.

Відео: https://www.youtube.com/watch?v=6mTbuzafcII


8

Я знайшов приклади читання від перетворювачів-js, що допомагає мені зрозуміти їх конкретно, як я можу використовувати їх у щоденному коді.

Наприклад, розглянемо цей приклад (взятий з README за посиланням вище):

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]

Для одного, використання xfвиглядає набагато чистіше, ніж звичайна альтернатива з підкресленням.

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

Приклад прикладу перетворювачів настільки довший. Версія підкреслення виглядає набагато більш стисло
appshare.co

1
@Zubair Не дужеt.into([], t.comp(t.map(inc), t.filter(isEven)), [0,1,2,3,4])
Хуан Кастаньеда

7

Перетворювачі - це (наскільки я розумію!) Функції, які беруть одну зменшувальну функцію і повертають іншу. Функція відновлення - це та, яка

Наприклад:

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

У цьому випадку мій перетворювач виконує функцію фільтрації входу, яка застосовується до 0, якщо це значення є парним? у першому випадку фільтр передає це значення лічильнику, потім він фільтрує наступне значення. Замість того, щоб спочатку відфільтрувати, а потім передати всі ці значення для підрахунку.

Це те саме, що у другому прикладі вона перевіряє одне значення за раз, і якщо це значення менше 3, то воно дозволяє рахувати додавання 1.


Мені сподобалося це просте пояснення
Ігнасіо

7

Чітке визначення перетворювача тут:

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.

Щоб зрозуміти це, розглянемо наступний простий приклад:

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

Що про це ми хочемо знати, скільки дітей у селі? Ми можемо легко знайти це за допомогою наступного редуктора:

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

Ось ще один спосіб зробити це:

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

Крім того, він дійсно потужний і при врахуванні підгруп. Наприклад, якщо ми хотіли б знати, скільки дітей в сім'ї Браун, ми можемо виконати:

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

Сподіваюся, ви зможете знайти корисні ці приклади. Ви можете знайти більше тут

Сподіваюся, це допомагає.

Клеменсіо Моралес Лукас.


3
"Перетворювачі - це потужний і компонований спосіб побудови алгоритмічних перетворень, які ви можете повторно використовувати в багатьох контекстах, і вони приходять до ядра Clojure і core.async." Визначення може стосуватися майже нічого?
appshare.co

1
Я б сказав майже до будь-якого перетворювача Clojure.
Клеменсіо Моралес Лукас

6
Це скоріше заява про місію, ніж визначення.
Марс

4

Про це я розповів із прикладом clojurescript, який пояснює, як функції послідовності тепер розширюються завдяки можливості замінити функцію відновлення.

Це сенс перетворювачів, коли я його читаю. Якщо ви думаєте про consабоconj операціях , яка закодована в таких операціях , як map, і filterт.д., відновлювальна функція була недосяжна.

З перетворювачами функція відновлення від'єднується, і я можу замінити її так, як я це робив з нативним масивом JavaScript pushзавдяки перетворювачам.

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

filter і друзі мають нову операцію 1 arity, яка повертає перетворюючу функцію, яку ви можете використовувати для постачання власної функції скорочення.


4

Ось мій (в основному) відповідь на жаргон та код.

Розгляньте дані двома способами, потоком (значення, що виникають у часі, наприклад, події) або структурою (дані, які існують у певний момент часу, такі як список, вектор, масив тощо).

Існують певні операції, які ви хочете виконати над потоками чи структурами. Однією з таких операцій є картографування. Функція картографування може збільшити кожен елемент даних (якщо припустити, що це число) на 1, і ви можете сподіватися уявити, як це могло б застосуватись до потоку чи структури.

Функція відображення - це лише одна із класів функцій, яку іноді називають "скорочуючими функціями". Інша поширена функція зменшення - це фільтр, який видаляє значення, що відповідають предикату (наприклад, видалити всі рівні значення).

Перетворювачі дозволяють «обгортати» послідовність однієї або декількох скорочуючих функцій і виробляти «пакет» (який сам є функцією), який працює як на потоках, так і на структурах. Наприклад, ви можете "упакувати" послідовність функцій скорочення (наприклад, відфільтрувати парні числа, потім зіставити отримані числа, щоб збільшити їх на 1), а потім використати цей "перетворювач" пакета або на потік або структуру значень (або на обох) .

Тож у чому це особливе? Як правило, відновлювальні функції не можуть бути ефективно складені для роботи як на потоках, так і на структурах.

Тож користь для вас полягає в тому, що ви можете використовувати свої знання навколо цих функцій і застосовувати їх до більшої кількості випадків використання. Вартість вам полягає в тому, що вам доведеться навчитися додатковій техніці (тобто датчику), щоб дати вам додаткову потужність.


2

Наскільки я розумію, вони схожі на будівельні блоки , відокремлені від введення та виведення. Ви просто визначите операцію.

Оскільки реалізація операції не входить до коду вводу і нічого не робиться з виходом, перетворювачі надзвичайно багаторазові. Вони нагадують мені про Flow s в потоках Akka .

Я також новачок у перетворювачах, вибачте за можливо незрозумілу відповідь.


1

Я вважаю, що ця публікація надає вам більше перегляду з пташиного польоту.

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


3
Відповіді, що покладаються лише на зовнішні посилання, не відволікаються на SO, оскільки посилання можуть розірватися в будь-який час у майбутньому. Цитуйте вміст у своїй відповіді.
Вінсент Кантін

@VincentCantin Фактично середній пост було видалено.
Дмитро Зайцев

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.