Clojure: Трансдьюсеры
Как уже было упомянуто, по умолчанию обработка коллекций в 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, а затем выбор только четных элементов (фильтрация).
Упражнение не проходит проверку — что делать? 😶
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
- Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет 🤨
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Мой код отличается от решения учителя 🤔
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Прочитал урок — ничего не понятно 🙄
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.