iT邦幫忙

2022 iThome 鐵人賽

DAY 16
0

[Day16] Clojure Macro (3) as-> / cond-> / some->

早安!

昨天公司大大分享色彩繽紛、香氣十足的南洋小甜點,讓debug鴨鴨十分開心

昨天介紹完了 thread-first -> 與 thread-last ->>

今天要來介紹一些變形怪 包含箭頭的API
as-> / cond-> / some-> 系列

(看了文情並茂的

有人是用 versatile來描述as-> :P
聽起來就是十分厲害!

首先要介紹的是多才多藝的 as-> 趕快來認識一下吧!

as->

昨天舉的thread first 例子極為單純,直接是數值的加減乘除運算,

因此直接用def來定義變數:

(clojure觀點:def是special form,用來把symbol number和值4關聯起來):

(def number 4)

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

=>14

但在真實世界可能不是這麼單純的... :P

被丟進thread的東東可能很長,
且可能又串加Flow control的判斷式,

所以就到了as->出場的時候啦!用法是這樣滴:

(as-> expr name & forms)

Binds name to expr, 

evaluates the first form in the lexical context
of that binding, 

then binds name to that result, repeating for each successive form, 

returning the result of the last form.

as-> 可以負責naming(也就是binding,綁定) 來代表某個在thread裡流動的東東

看起來漏漏長不好懂,
那我們用最簡單的萬用條件式(if true / if false) 來學習 as-> 基礎語法吧

as-> example

  • 第一個參數是值99 (a value to be threaded through the following forms)

  • 第二個參數是值binding name number (The second argument is the name of a binding)

(as-> 99 number
  (inc number)
  (if false
    (inc number)
    number))
=> 100

99先+1,如果false再加一,但因為不滿足條件,所以結果只有100

(as-> 99 number
  (inc number)
  (if true
    (inc number)
    number))
=> 101

99先+1,如果true再加一,滿足true的條件,所以結果變成100

這樣應該很容易掌握吧!

as-> 複雜一咪咪的例子

  • fruit搭配 update / reduce往下串

身為一個健康的孩只,冰箱裡總是有很多種水果

因此我把水果放在map裡,用as-> binding適合的namingfruit

當蘋果只剩1顆的時候,我會再去超市補給,補回5顆

(as-> {:apple 1 :orange 3 :banana 5} fruit
  (update fruit :apple + 4)
  (reduce (fn [s [_ v]] (+ s v)) 0 fruit))
  
=> 13

所以目前冰箱總共有個數為13件的水果~

cond vs. cond->

討論完了 as->,用cond複習一個超簡單的例子,

慶祝一下週五的來臨~~

(defn week-days
  "Determines whether we can enjoy holiday or go to work"
  [day]
  (cond
    (= day 0) "Sunday"
    (= day 5) "Thank GOD it is Friday!"
    (= day 6) "Saturday"
    :else "Weekday"))

(week-days (.getDay (java.util.Date.))) 
=> "Thank GOD it is Friday!"

那cond和cond->的差別在哪呢?

比較一下語法結構,cond->cond->> 會在判斷句的前面多加一個expression

如果有多個test expression,它會去跑完每一個expression,

當想要把evaluate表達式串在一起時非常有用

  • cond
(cond & clauses)

Takes a set of test/expr pairs. 
It evaluates each test one at a time.  

If a test returns logical true, 
cond evaluates and returns the value of the corresponding expr 
and doesn't evaluate any of the other tests or exprs. 

(cond) returns nil.
  • cond->
(cond-> expr & clauses)

Takes an expression and a set of test/form pairs. 

Threads expr (via ->) through each form for which the corresponding test expression is true. 

Note that, unlike cond branching, 
cond-> threading does not short circuit 
after the first true test expression.

cond-> 搭配 只有一個條件branch

10 true的話加2,結果為8

(cond-> 10 true (- 2)
=> 8

10 false的話減2,結果不變,還是10

(cond-> 10 false (- 2)
=> 10

cond-> 搭配 一個以上的條件branch

以下例子會判斷完所有條件,遞增了兩次,變成102

(cond-> 100
  100 inc
  nil inc
  false inc
  true inc
)

=> 102

參考這篇文章The usefulness of Clojure's cond->

當我們要從map選出並與 assoc, update, or dissoc 搭配修改資料時非常好用.
assoc / assoc-in / update / update-in 也是預計之後會再花篇幅來講解說明:)

some->

先列一下some-> vs some->> API 說明:

  • some->
(some-> expr & forms)

When expr is not nil, 
threads it into the first form (via ->),
and when that result is not nil, through the next etc
  • some->>
(some->> expr & forms)

When expr is not nil, 
threads it into the first form (via ->>),
and when that result is not nil, through the next etc

當我們有時需要guard against null pointer exceptions,就會用到some-> / some->>

some-> example

例如裝水果的map

;; 幫我把orange +1!!
(-> {:apple 1 :orange 3} :orange inc)
=> 4

如果找不到對應key的話,會噴錯 NullPointerException

;; 幫我把banana +1!! 
(-> {:apple 1 :orange 3} :banana inc)

Execution error (NullPointerException) at tutorial.core/eval2262 (form-init2138656939186993434.clj:1).
null

但如果用 some-> guard保護一下,就會回傳nil

(some-> {:apple 1 :orange 3} :banana inc)
=> nil

是不是覺得比較心安了呢?

同場加映: -> thread-first / ->> thread-last大亂鬥

我們說過

thread-first macro ->適合表示把某個data structure裡的東西一層層拿出來,

然後 map, filter, remove, reduce, into``->>適合使用thread-last macro ->>

舉個例子同時有->->>的例子,
目的是來把大家搞暈 讓大家知道各種組合技 XD

使用Java substring() method在clojure的語法:

(-> [:ting :bater]
  (->> (map name))  ;; ("ting" "bater")
  (first)           ;; "ting"
  (.substring 1))  

猜猜看結果會是什麼呢?

=> "ing"

用macroexpand展開

;; 

(macroexpand '(.substring (first(-> [:ting :bater]  (->> (map name)))) 1))

=> (. (first (-> [:ting :bater] (->> (map name)))) substring 1)


;; 執行展開式
(. (first (-> [:ting :bater] (->> (map name)))) substring 1)

=> "ing"

延伸思考:

經過這10幾天的旅程,第二天及昨天的我再度提到偶數平方和->>例子

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

我們已經知道,可以透過匿名函式簡化(少包一層括弧)成

(->> (range 50)
     (filter even?)
     (map #(* % %))
     (reduce +))

看了clojurian的slack討論串

有大大(@seancorfield)提到也可以改成這樣的寫法:

(transduce (comp (filter even?) (map #(* % %))) + (range 50))

這種方式不會 create intermediate lazy sequences

那到底什麼是 lazy sequences 懶惰的序列呢?
明天就要來挑戰這個(小妹在下認為)clojure最艱深的主題之一lazy sequences啦!

狀態顯示為週五lazy懶洋洋~ :P


上一篇
[Day15] Clojure Macro (2) thread-first -> / thread-last ->>
下一篇
[Day17] Clojure Laziness (1) Lazy evaluation 懶惰求值
系列文
後端Developer實戰ClojureScript: Reagent與前端框架 Reframe30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Bater
iT邦新手 4 級 ‧ 2022-09-30 10:44:08

看到我了名字出現在範例中,給個推~! 隔幾天沒有追上進度就有點看不懂,果然需要一篇一篇好好追緊才行。這種箭頭的用法有點像我熟悉的coffeescript,看起來很親切。
/images/emoticon/emoticon07.gif

貓饅頭好謙虛也好親切(跟coffeescript一樣XD)~
相信你也可以變成clojure達人的~/images/emoticon/emoticon08.gif

Bater iT邦新手 4 級 ‧ 2022-09-30 11:10:05 檢舉

/images/emoticon/emoticon62.gif

我要留言

立即登入留言