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

Clojure: Создание трансдьюсеров

В прошлом уроке мы научились комбинировать трансдьюсеры, в этом уроке научимся создавать свои трансдьюсеры! Зачем? Для того, если обработку коллекций не получается свести к простым операциям map, filter, reduce. Для создания трансдьюсеров требуется соблюдать следующую форму:

(defn my-transduce-fn
  ([] ...)
  ([result] ...)
  ([result input] ...)))

Получается, чтобы создать трансдьюсер, нужно как минимум объявить 3 варианта обработки коллекции (на самом деле два, для 0 арности и 1 арности):

  • Init функция (арность функции 0), основное действие, которое будет применено к коллекции при применении трансдьюсера my-transduce-fn.
  • Completion функция (арность функции 1), не все обработки коллекций сводятся к какому-то итоговому значению, однако для тех, которые сводятся (например transduce), completion функция как раз и используется для получения итогового значения. В этой функции трансдьюсер my-transduce-fn должен быть вызван ровно один раз.
  • Step функция (арность функции 2), эта функция применяется на каждом шаге при свертке коллекции, в ней my-transduce-fn вызывается с арностью 0 или больше, в зависимости от логики, которую реализует трансдьюсер. Например, при использовании filter трансдьюсер my-transduce-fn может быть применен или нет в зависимости от предиката, при использовании в map, трансдьюсер применится ровно один раз, в случае с функцией cat - трансдьюсер может быть применен несколько раз, в зависимости от переданной коллекции.

Описание может немного запутать, поэтому для практики сделаем несколько трансдьюсеров:

; трансдьюсер для инкремента коллекции на 1
(defn increment-all
  ([] (map inc))
  ([coll] (sequence (increment-all) coll)))

; трансдьюсер для фильтрации четных чисел
(defn filter-evens
  ([] (filter even?))
  ([coll] (sequence (filter-evens) coll)))

; трансдьюсер для умножения всех элементов на 2
(defn double-all
  ([] (map #(* % 2)))
  ([coll] (sequence (double-all) coll)))

; трансдьюсер для умножения всей коллекции на свои элементы (поиск произведения коллекции)
(defn product
  ([] *)
  ([coll] (reduce (product) 1 coll))
  ([xform coll] (transduce xform (product) 1 coll)))

А теперь потестируем полученные трансдьюсеры:

; классический подход с иммутабельными данными
; используется макрос `->>` для удобства композиции функций с точки зрения организации кода
(->> (range 15)
     (map inc)
     (filter even?)
     (map #(* % 2))
     (reduce * 1))
82575360

; версия с трансдьюсерами
(def my-transducer ; соберем все операции в один трансдьюсер
  (comp
    (increment-all)
    (filter-evens)
    (double-all)))

(product my-transducer (range 15))
82575360

; а теперь проверим время обработки для двух случаев
(time
  (->> (range 15)
       (map inc)
       (filter even?)
       (map #(* % 2))
       (reduce * 1)))

"Elapsed time: 0.226162 msecs"
82575360

(time (product my-transducer (range 15)))
"Elapsed time: 0.154372 msecs"
82575360

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

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

Задание

Для закрепления, создайте три трансдьюсера, student-names - который извлекает из вектора первый элемент, представляющей собой имя/фамилию студента, lower-case-name - который переводит имена/фамилии студентов в нижний регистр, slugify-names - который меняет разделитель имени/фамилии студента с пробела на -. А затем скомбинируйте эти трансдьюсеры в один do-name-magic.

Пример:

(def students
  [["Luke Skywalker" "Jedi"]
   ["Hermione Granger" "Magic"]
   ["Walter White" "Chemistry"]])

(into [] do-name-magic students)
["luke-skywalker" "hermione-granger" "walter-white"]
Упражнение не проходит проверку — что делать? 😶

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

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

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

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

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

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

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

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

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

Полезное


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