Трединг-макросы -> и ->>

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

Трединг-макрос (threading macro) — конструкция, которая «протягивает» значение через цепочку функций, делая код читаемым сверху вниз.

Проблема глубокой вложенности

Вспомним пример из прошлого урока: (reduce + (map sq (filter even? coll))). Читать его нужно изнутри наружу — сначала filter, потом map, потом reduce. Чем длиннее цепочка, тем труднее. Трединг-макросы переворачивают запись в естественный порядок.

Макрос -> (thread-first)

Макрос -> берёт первое значение и подставляет его первым аргументом в следующий вызов, затем результат — первым аргументом в следующий, и так далее.

; без макроса
(assoc (assoc {} :a 1) :b 2)

; с -> читается сверху вниз
(-> {}
    (assoc :a 1)
    (assoc :b 2))
; => {:a 1, :b 2}

Вывод:

{:a 1, :b 2}

Здесь {} подставляется первым аргументом в (assoc :a 1), давая (assoc {} :a 1), затем результат — в следующий assoc.

Макрос ->> (thread-last)

Макрос ->> подставляет значение последним аргументом. Это идеально для map, filter, reduce, у которых коллекция стоит в конце.

(->> [1 2 3 4 5 6]
     (filter even?)
     (map #(* % %))
     (reduce +))
; чётные -> квадраты -> сумма => 56

Вывод:

56

Тот же расчёт, что в прошлом уроке, но теперь читается как пайплайн: «взять вектор → отфильтровать чётные → возвести в квадрат → сложить».

Когда какой

МакросКуда подставляетДля чего
->первым аргументомработа с map: assoc, update, get
->>последним аргументомработа с коллекциями: map, filter, reduce

Как работает под капотом

Это макросы (а не функции): они работают с кодом как с данными ещё до выполнения. На этапе компиляции -> переписывает цепочку обратно во вложенные вызовы. То есть (-> x (f a) (g b)) разворачивается в (g (f x a) b). В рантайме никакой дополнительной работы нет — это чисто синтаксическое преобразование, и поэтому threading ничего не стоит по скорости. Здесь напрямую видна польза гомоиконности: макрос меняет структуру кода.

Частые ошибки

  • Перепутать -> и ->>. Для коллекций (map/filter) нужен ->>, для map-структур (assoc/update) — ->.
  • Threading там, где один вызов. Для единственной операции макрос лишний и только запутывает.
  • Смешивать порядки аргументов. Если в цепочке функции с разным положением аргумента, бывает нужен as-> (более гибкий вариант).

Итоги

  • Трединг-макросы превращают вложенные вызовы в линейный пайплайн.
  • -> подставляет значение первым аргументом (для map-структур).
  • ->> подставляет последним аргументом (для коллекций: map/filter/reduce).
  • Это синтаксическое преобразование на этапе компиляции — бесплатно по скорости.
Проверьте себя
1. Куда макрос -> подставляет протягиваемое значение?
AПоследним аргументом следующего вызова
BПервым аргументом следующего вызова
CВ случайную позицию
DВ виде ключа map
2. Какой макрос удобнее для цепочки filter → map → reduce?
A->
B->>
Cas->
Ddoto
3. Когда выполняется разворачивание трединг-макроса в обычные вызовы?
AВ рантайме при каждом вызове
BНа этапе компиляции, без затрат в рантайме
CТолько в REPL
DНикогда, это функция