Как уже было упомянуто, по умолчанию обработка коллекций в Clojure знаменитой тройкой map
, filter
и reduce
иммутабельна, что иногда может сильно увеличить расход памяти и время обработки коллекции большого размера. Однако в таких ситуациях на помощь приходят трансдьюсеры.
Трансдьюсеры - это составные алгоритмические преобразования над коллекциями. Они независимы от контекста своих входных и выходных параметров и задают только суть преобразования в терминах отдельного элемента коллекции. Поскольку трансдьюсеры не зависят от ввода или вывода, их можно использовать в различных процессах - коллекциях, потоках, каналах и т. д.
Редукционная функция - это такая функция, которую вы передаете для редукции, то есть - это функция, которая принимает накопленный результат и новую коллекцию и возвращает новый накопленный результат:
;; сигнатура редукционной функции
что угодно, вход -> что угодно
Трансдьюсер (иногда называемый xform
или xf
) - это преобразование одной редуцирующей функции в другую:
;; сигнатура преобразователя
(что угодно, вход -> что угодно) -> (что угодно, вход -> что угодно)
А теперь попробуем сделать свою трансформацию коллекции с помощью трансдьюсеров:
(filter odd?) ;; этот вызов возвращает трансдьюсер, который фильтрует нечетные числа
(map inc) ;; этот вызов возвращает трансдьюсер, который инкрементирует все числа на 1
(take 5) ;; этот вызов возвращает трансдьюсер, который берет из коллекции первые 5 чисел
Трансдьюсеры компонуются с помощью обычной функциональной композиции. Рекомендуемый способ компоновки трансдьюсеров - это использование существующей функции comp
:
(def xf
(comp
(filter odd?)
(map inc)
(take 5)))
Трансдьюсер xf
- это стек преобразований, который будет применен к переданным элементам. Каждая функция в стеке выполняется перед операцией, которую она обертывает. Композиция трансдьюсеров выполняется справа налево, но строит стек преобразований, который выполняется слева направо (в этом примере фильтрация происходит перед маппингом).
В качестве мнемоники запомните, что упорядочивание функций трансдьюсеров в comp
имеет тот же порядок, что и преобразования последовательности в макросе ->>
. Пример выше эквивалентен следующему примеру:
(->> coll
(filter odd?)
(map inc)
(take 5))
Функции, которые можно использовать в качестве трансдьюсеров: map
, cat
, mapcat
, filter
, remove
, take
, take-while
, take-nth
, drop
, drop-while
, replace
, partition-by
, partition-all
, keep
, keep-indexed
, map-indexed
, distinct
, interpose
, dedupe
, random-sample
.
А теперь посмотрим, как применять трансдьюсеры. Самым распространенным способом применения трансдьюсеров является использование функции transduce
, которая является аналогом функции reduce
, только для трансдьюсеров:
(transduce xform f coll)
(transduce xform f init coll)
transduce
нелениво редуцирует coll
с помощью преобразователя xform
, применяемого к редуцирующей функции f
, используя init
в качестве начального значения, если оно предоставлено, или f
в противном случае. f
функция знает о том, как накапливать результат. А теперь примеры:
(def xf (comp (filter odd?) (map inc)))
(transduce xf + (range 5))
;; => 6
(transduce xf + 100 (range 5))
;; => 106
into
позволяет применять трансдьюсер к входной коллекции и создать на основе результата применения трансдьюсера новую коллекцию. Старайтесь по возможности использовать эту функцию, так как она применяет reduce
эффективнее (без промежуточных коллекций), если есть такая возможность.
(into [] xf (range 10))
[2 4 6 8 10]
(into () xf (range 10)) ; => в списки элементы добавляются иначе, поэтому итоговый список в обратном порядке
(10 8 6 4 2)
(into #{} xf (range 10))
#{4 6 2 10 8}
Про создание трансдьюсеров поговорим в следующем упражнении.
Скомбинируйте несколько трансдьюсеров в один my-xf
, в котором происходит умножение всех элементов на 10, затем деление на 5, а затем выбор только четных элементов (фильтрация).
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.
Ваше упражнение проверяется по этим тестам
1(ns transducers-test
2 (:require [test-helper :refer [assert-solution]]
3 [index :refer [my-xf]]))
4
5(assert-solution
6 [[(range 10)] [(range 50)] [(range 90)]]
7 [90 2450 8010]
8 #(transduce my-xf + %))
9
10(assert-solution
11 [[(range 5)] [(range 10)]]
12 [[0 2 4 6 8] [0 2 4 6 8 10 12 14 16 18]]
13 #(into [] my-xf %))
Решение учителя откроется через: