Может ли кто-нибудь объяснить мне преобразователи Clojure в простых терминах?


101

Я пробовал читать об этом, но до сих пор не понимаю их ценности и того, что они заменяют. И они делают мой код короче, понятнее или как?

Обновить

Многие публиковали ответы, но было бы неплохо увидеть примеры с преобразователями и без них для чего-то очень простого, что может понять даже такой идиот, как я. Если, конечно, преобразователи не нуждаются в определенном высоком уровне понимания, и в этом случае я никогда не пойму их :(

Ответы:


75

Преобразователи - это рецепты того, что делать с последовательностью данных, не зная, какова основная последовательность (как это сделать). Это может быть любой seq, асинхронный канал или, возможно, наблюдаемый.

Они компонуются и полиморфны.

Преимущество в том, что вам не нужно реализовывать все стандартные комбинаторы каждый раз, когда добавляется новый источник данных. Опять и опять. В результате вы, как пользователь, можете повторно использовать эти рецепты в разных источниках данных.

Обновление объявления

В предыдущей версии Clojure 1.7 у вас было три способа писать запросы потока данных:

  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.
user100464

7
Преобразователи кажутся полезным способом абстрагироваться от различных форм повторяемых объектов. Они могут быть нерасходуемыми, например, Clojure seqs, или расходными (например, асинхронными каналами). В этом отношении мне кажется, что вы получите большую выгоду от использования преобразователей, если, например, переключитесь с реализации на основе seq на реализацию core.async с использованием каналов. Преобразователи должны позволить вам сохранить суть вашей логики неизменной. Используя традиционную обработку на основе последовательностей, вам придется преобразовать это, чтобы использовать либо преобразователи, либо какой-либо аналог core-async. Это бизнес-кейс.
Натан Дэвис,

47

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

Это приличный пробег .

По сравнению с сочиняли вызовы к старому map, filter, и reduceт.д. Вы получаете более высокую производительность , так как вам не нужно , чтобы построить промежуточные коллекции каждого шага, и несколько раз ходить эти коллекции.

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


2
Просто любопытно, вы сказали выше: «создавать промежуточные коллекции между каждым шагом». Но разве «промежуточные коллекции» не звучат как антипаттерн? .NET предлагает ленивые перечисления, Java предлагает ленивые потоки или итерации, управляемые Guava, в ленивом Haskell тоже должно быть что-то ленивое. Ни один из них не требует map/ reduceиспользования промежуточных коллекций, потому что все они создают цепочку итераторов. Где я здесь не прав?
Любомир Шайдарев

3
Закрытие mapи filterсоздание промежуточных коллекций при вложении.
шумовой мастер

4
И, по крайней мере, что касается версии лени в Clojure, проблема лени здесь ортогональна. Да, map и filter являются ленивыми, они также создают контейнеры для ленивых значений, когда вы их связываете. Если вы не держитесь за голову, вы не создадите большие ленивые последовательности, которые не нужны, но вы все равно создадите эти промежуточные абстракции для каждого ленивого элемента.
noisesmith

Пример будет хорош.
appshare.co

8
@LyubomyrShaydariv Под «промежуточной коллекцией» noisesmith не означает «итерация / повторное изменение всей коллекции, а затем повторение / повторное изменение другой коллекции». Он или она означает, что когда вы вкладываете вызовы функций, возвращающие последовательности, каждый вызов функции приводит к созданию новой последовательности. Фактическая итерация по-прежнему выполняется только один раз, но из-за вложенных последовательностей происходит дополнительное потребление памяти и выделение объектов.
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?
Марс

22

Допустим, вы хотите использовать серию функций для преобразования потока данных. Оболочка Unix позволяет делать такие вещи с помощью оператора pipe, например

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

(Приведенная выше команда подсчитывает количество пользователей, у которых в имени пользователя есть буква r в верхнем или нижнем регистре). Это реализовано в виде набора процессов, каждый из которых считывает выходные данные предыдущего процесса, поэтому существует четыре промежуточных потока. Вы можете представить себе другую реализацию, которая объединяет пять команд в одну агрегированную команду, которая будет читать из своего ввода и записывать свой вывод ровно один раз. Если бы промежуточные потоки были дорогими, а композиция - дешевой, это могло бы быть хорошим компромиссом.

То же самое и с Clojure. Существует несколько способов выразить конвейер преобразований, но в зависимости от того, как вы это делаете, вы можете получить промежуточные потоки, переходящие от одной функции к другой. Если у вас много данных, быстрее объединить эти функции в одну функцию. Преобразователи позволяют легко это сделать. Более ранняя инновация Clojure, редукторы, тоже позволяет делать это, но с некоторыми ограничениями. Преобразователи снимают некоторые из этих ограничений.

Итак, чтобы ответить на ваш вопрос, преобразователи не обязательно сделают ваш код короче или более понятным, но ваш код, вероятно, также не будет длиннее или менее понятным, и если вы работаете с большим количеством данных, преобразователи могут сделать ваш код Быстрее.

Это довольно хороший обзор преобразователей.


1
А, значит, преобразователи в основном предназначены для оптимизации производительности, это то, что вы говорите?
appshare.co

@Zubair Да, верно. Обратите внимание, что оптимизация выходит за рамки исключения промежуточных потоков; вы также можете выполнять операции параллельно.
user100464

2
Стоит упомянуть pmap, что, кажется, не привлекает достаточно внимания. Если вы mapпроверяете дорогостоящую функцию над последовательностью, сделать операцию параллельной так же просто, как добавить «p». Больше ничего менять в коде не нужно, и он доступен сейчас - ни альфа, ни бета. (Если функция создает промежуточные последовательности, тогда преобразователи могут быть быстрее, я полагаю.)
Марс

10

Рич Хики выступил с докладом «Transducers» на конференции Strange Loop 2014 (45 мин).

Он просто объясняет, что такое преобразователи, на реальных примерах - обработка сумок в аэропорту. Он четко разделяет различные аспекты и противопоставляет их нынешним подходам. Ближе к концу он дает обоснование их существования.

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


8

Я обнаружил, что чтение примеров из transducers-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выглядит намного чище, чем обычная альтернатива с Underscore.

_.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

В этом случае my-transducer принимает функцию входной фильтрации, которая применяется к 0, тогда, если это значение четное? в первом случае фильтр передает это значение счетчику, а затем фильтрует следующее значение. Вместо того, чтобы сначала фильтровать, а затем передавать все эти значения для подсчета.

То же самое и во втором примере: он проверяет одно значение за раз, и если это значение меньше 3, он позволяет count добавить 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 Transducer.
Клеменсио Моралес Лукас

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 в Akka Streams .

Я также новичок в датчиках, извините за возможно неясный ответ.


1

Я считаю, что этот пост дает вам более полное представление о датчике с высоты птичьего полета.

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


3
Ответы, основанные только на внешних ссылках, не приветствуются в SO, поскольку ссылки могут сломаться в любой момент в будущем. Вместо этого цитируйте содержание своего ответа.
Винсент Кантин

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.