iT邦幫忙

2022 iThome 鐵人賽

DAY 15
0

[Day15] Clojure Macro (2) thread-first -> / thread-last ->>

鴨子划水中,不知不覺就完賽1/2了!(給自己一個讚)

狀態顯示為每天早上趕鐵人賽稿來不及吃早餐 XD

今天要講的是
thread-first macro (->) vs thread-last macro (->>)

其實呀!在第二天的文章的例子,就有默默地鋪梗了:

以下的程式代表求取50以內的偶數平方和:

(->> (range 50)
     (filter even?)
     (map (fn [x] (* x x)))
     (reduce +))
     
=> 19600

我已經佈這個局佈了兩週之久 XD

而且更棒的是,上面提到的function (range / filter / even? / map / 匿名函數 / reduce)

現在是不是都看得懂了呢?

看不懂的也不要緊,可以再回去複習本系列前10篇文章 ;)

Macro溫故知新

Macro是重組code的語法,

從這張流程圖(Reference)發現:
macro expansion time在reader之後compile(eval)之前,會將code重組完成

至於什麼是->>-> 呢?他們是怎麼重組code

待我們閱讀clojure doc後細細說明!

->>->: (Threading Macro / Arrow Macro)

->>->也是Macro的一種

它們會幫忙把複雜的nested function calls
轉換成線性的流程 (linear flow of function calls),
讓人眼看起來是照code字面上順序執行的感覺

來比較API的說明:

->

(-> x & forms)

Threads the expr through the forms. 

Inserts x as the **second** item in the first form, making a list of it if it is not a list already. 

If there are more forms, 
inserts the first form as the second item in second form, etc.
->>

(->> x & forms)

Threads the expr through the forms. 

Inserts x as the **last** item in the first form, 
making a list of it if it is not a list already. 

If there are more forms, 
inserts the first form as the last item in second form, etc.

thread-first macro ->

rest

為了舉例講解->的例子,

在這裡介紹一個新的API出場:rest (請掌聲鼓勵鼓勵)

rest會回傳除了第一個之外、剩下的item sequence

rest

(rest coll)

Returns a possibly empty seq of the items after the first. 
Calls seq on its argument.

來試試看:

(rest [1 2 3 4 5])
=> (2 3 4 5)

(rest nil)
=> ()

484剛好跟 first用途相反!

first

(first coll)

Returns the first item in the collection. Calls seq on its argument. 
If coll is nil, returns nil.
;;拿出第一個
(first [1 2 3 4 5])
=> 1

(first nil)
=> nil

-> 搭配 rest

這裡有個1到6的vector,

  • 先reverse,再拿出除了第一個item的其餘sequence
(-> [1 2 3 4 5 6] reverse rest)
=> (5 4 3 2 1)

macroexpand展開看看:

(macroexpand '(-> [1 2 3 4 5 6] reverse rest))
=> (rest (reverse [1 2 3 4 5 6]))


(rest (reverse [1 2 3 4 5 6]))
=> (5 4 3 2 1)

把reverse放在rest後面呢?

  • 先拿出除了第一個item的其餘sequence,再reverse
 (-> [1 2 3 4 5 6] rest reverse)
=> (6 5 4 3 2)

macroexpand展開看看:

(reverse (rest [1 2 3 4 5 6]))
(6 5 4 3 2)

 (macroexpand '(-> [1 2 3 4 5 6] rest reverse))
=> (reverse (rest [1 2 3 4 5 6]))

-> 取出再複雜一點的data structure

以上用vector的例子比較不容易理解 ->的威力

我們來個複雜點的map

 (def person 
      {:name "Ting"
         :address {:street "154 加蚋路"
                   :city "貓貓市"
                   :zip 520}
         :employer {:name "Abagile"
                    :address {:street "華爾街"
                              :city "鴨鴨市"
                              :zip 888}}})

想知道Ting在哪裡上班的話,
我們可以用 -> 來表達inserts the first form as the second item in second form

 (-> person :employer :address :street)
=> "華爾街"

-> 實務上常用的地方

-> 常用來搭配 assoc and update data manipulation之類的function

(-> field 
(name ,,,)
(keyword ,,,))
(-> :person
    (assoc :hair-color :gray)
    (update :age inc))

->->>順序比較(重要!)

以下的算式求出來的結果是 ((4 + 2) -1) * 3 = 15

(def number 4)

(-> number (+ 2) (- 1) (* 3))
15

如果換成->>就完全不同囉

(def number 4)

(->> number (+ 2) (- 1) (* 3))
-15
 (->> number (+ 2) (- 1) (* 3))

其實是

  (* 3 (- 1 (+ 2 number)))

的意思

為什麼會這樣呢? 關鍵在那個 -1,算出來的參數會插在後面

(- 1 (參數) )

所以以人類的肉眼來看是 (1 - (2 + 4)) * 3 = -15 (負15)

真是正負兩極呀~~~

'clojure.walk 拆解 macroexpand-all 執行順序

如果還是不相信的話,可以使用macroexpand-all展開箭頭macro的廬山真面目~

(use 'clojure.walk)

(macroexpand-all '(-> number (+ 2) (- 1) (* 3)))

=> (* (- (+ number 2) 1) 3)

;;

(macroexpand-all '(->> number (+ 2) (- 1) (* 3)))

=> (* 3 (- 1 (+ 2 number)))

thread-last macro ->>

回到本文一開始的例子:求取50以內的偶數平方和

用昨天介紹的macroexpand展開:

tutorial.core=> (macroexpand '(->> (range 50)
           #_=>      (filter even?)
           #_=>      (map (fn [x] (* x x)))
           #_=>      (reduce +)))
           

會發現原型是我們一開始學的長長一條clojure語法

(reduce + (map (fn [x] (* x x)) (filter even? (range 50))))

翻譯蒟蒻:

  • 先把用range把範圍內的值找出來
  • 再filter出偶數的值
  • 每個值平方
  • 把所有值相加

是不是有點像pipelines(管線)一樣,一個步驟處理完接著下個步驟呢?

map, filter, remove, reduce, into
常常使用thread-last macro ->>

as->

在有些情況會需要用到 as->,而不能用->>->

A pipeline may consist of function calls with varying insertion points. In these cases, you’ll need to use as->

the first argument is a value to be threaded through the following forms.
The second argument is the name of a binding

至於舉例的話~ 嘿嘿先賣個關子

下期預告:clojure API介紹

同個箭牌系列的有好多,大家不要暈囉~

as->

cond->
cond->>

some->
some->>

明天都來一網打盡吧~!


上一篇
[Day14] Clojure Macro (1) 初探 Macro
下一篇
[Day16] Clojure Macro (3) as-> / cond-> / some->
系列文
後端Developer實戰ClojureScript: Reagent與前端框架 Reframe30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言