Бесплатный курс по Clojure. Зарегистрируйтесь для отслеживания прогресса →

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, а затем выбор только четных элементов (фильтрация).

Упражнение не проходит проверку — что делать? 😶

Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:

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

Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.

Мой код отличается от решения учителя 🤔

Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.

В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.

Прочитал урок — ничего не понятно 🙄

Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.

Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.

Полезное


Нашли ошибку? Есть что добавить? Пулреквесты приветствуются https://github.com/hexlet-basics
Если вы столкнулись с трудностями и не знаете, что делать, задайте вопрос в нашем большом и дружном сообществе