Потокові макроси
Потокові макроси, також відомі як функції–стрілки, використовуються для запису вкладених викликів функцій у простішому форматі, що більш зрозумілий для читання.
Уявіть, що у вас є вираз (f (g (h x))), в якому функція f отримує результат обчислення функції g і так далі, з навіть більшою кількістю вкладених викликів. За допомогою потокового макросу -> такий вираз можна записати як (-> x (h) (g) (f)), що набагато легше читати.
Це синтаксичний цукор, бо функція–стрілка насправді є макросом, що перетворює вираз (-> x (h) (g) (f)) в (f (g (h x))) на етапі компілювання коду в JavaScript.
Прийміть до уваги, що круглі дужки навколо h, g та f — необов'язкові, якщо ці функції не мають додаткових параметрів. Тобто вираз (f (g (h x))) можна записати, як (-> x h g f).
Макрос першого потоку (->)
Його називають макросом першого потоку тому, що він встановлює значення першого аргументу для всіх виразів.
Нумо розглянемо наступний приклад без потокового макросу:
(def book {:name "Lady of the Lake"
:readers 0})
(update (assoc book :age 1999) :readers inc)
;; => {:name "Lady of the lake" :age 1999 :readers 1}
З макросом -> це буде виглядати так:
(-> book
(assoc :age 1999)
(update :readers inc))
;; => {:name "Lady of the lake" :age 1999 :readers 1}
У ClojureScript функції, що трансформують структури даних завжди приймають дані першим аргументом. Тому цей потоковий макрос дуже зручний у використанні, коли треба записати підряд декілька таких трансформацій.
Макрос останнього потоку (->>)
Основна відмінність макросу останнього потоку від макросу першого потоку у тому, що макрос ->> встановлює значення останнім аргументом в усіх виразах.
Давайте розглянемо приклад:
(def numbers [1 2 3 4 5 6 7 8 9 0])
(take 2 (filter odd? (map inc numbers)))
;; => (3 5)
Той самий приклад з використанням макросу ->>:
(->> numbers
(map inc)
(filter odd?)
(take 2))
;; => (3 5)
Цей потоковий макрос дуже зручний у використанні, коли треба трансформувати послідовності або колекції даних. Бо у ClojureScript функції, що трансформують послідовності та колекції, завжди приймають дані останнім аргументом.
Потоковий макрос (as->)
Інколи може бути так, що макроси першого та останнього потоку незручно використовувати. В таких випадках використовується макрос as->, що дозволяє встановити значення на довільне місце для кожного виразу окремо, а не лише в початок чи кінець виразу.
Цей макрос приймає два значення і довільну кількість виразів. Як і в звичайному потоковому макросі, перший аргумент — це значення, яке буде встановлено аргументом у всі вирази. А от другим аргументом повинен бути символ, з яким воно буде зв’язане. Цей символ використовується у всіх виразах в макросі для встановлення значення на довільне місце в аргументах функцій.
Давайте розглянемо приклад:
(as-> numbers $
(map inc $)
(filter odd? $)
(first $)
(hash-map :result $ :id 1))
;; => {:result 3 :id 1}
Потокові макроси some-> та some->>
Ще одна пара потокових макросів для спеціальних випадків. Вони працюють майже так само, як макроси -> та ->>, але мають можливість припинити виконання послідовності виразів, якщо один з них поверне nil.
Розглянемо такий приклад:
(some-> (rand-nth [1 nil])
(inc))
;; => 2
(some-> (rand-nth [1 nil])
(inc))
;; => nil
Це простий спосіб уникнення помилок, коли nil передається в функцію, яка цього не чекає.
Потокові макроси cond-> та cond->>
Макроси cond-> та cond->> також схожі на -> та ->> . Але вони дозволяють пропускати виконання виразів за допомогою умови, що записується перед кожним виразом.
Розглянемо приклад:
(defn describe-number
[n]
(cond-> []
(odd? n) (conj "odd")
(even? n) (conj "even")
(zero? n) (conj "zero")
(pos? n) (conj "positive")))
(describe-number 3)
;; => ["odd" "positive"]
(describe-number 4)
;; => ["even" "positive"]
Тут виконуються лише ті вирази, для яких умова обчислюється в логічне true.