iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 13
0
Software Development

擁抱 Clojure系列 第 13

[第 13 天] 擁抱 Clojure:流程控制(三)

流程控制(三)

列表推導

Clojure 中的 for 與一般程式語言的 for 不同,它利用一個群集作爲來源,運用運算式以及條件式產生新的群集,這稱作列表推導 (List comprehension)。它接受類似 let 繫結綁定的方式,在向量中以符號繫結群集,之後的本體運算式將陸續被代入群集中的元素,本體運算式每次求值的結果則放入新的群集中,運行完畢後返回新的群集。

以下範例示範由代入的向量,產生向量中元素以及它的倍數組成一對的新向量:

(for [x [1 2 3 4 5]] [x (* x x)])
;; => ([1 1] [2 4] [3 9] [4 16] [5 25])

前面曾提到 doseq 運算式有幾個條件修飾子 (Modifier) 可以調整產生的結果,其實 doseq 是依靠 for 打造出來的,doseq 的修飾詞都是繼承自 for,三種條件修飾子都是 for 提供給 doseq。讓我們來看看 for 使用條件修飾子的樣貌:

(for [x (range 5)
      y (range 5)
      :let [z (+ x y)]
      :when (.isProbablePrime (BigInteger/valueOf z) 5)]
  (list x y))
;; => ((0 2) (0 3) (1 1) (1 2) (1 4) (2 0) (2 1) (2 3) (3 0) (3 2) (3 4) (4 1) (4 3))

以上範例中 xy 分別代表內容包含 0 到 4 的列表,當兩個向量中的元素相加起來爲質數,便將兩元素放入新群集裡。範例中示範了如何使用 Java 靜態方法與呼叫 BigInteger 的執行個體方法。與 Java 的溝通將於後續的文章中詳細介紹。

穿引巨集

Clojure 語言之中有非常多的巨集 (Macro),它擴充了語言基本提供的功能,帶來了易用與方便,在後續文章中將會有詳細的介紹。這裡先介紹的巨集,它爲程式碼帶來可讀性,稱爲穿引巨集 (Threading Macro)。

首位穿引

假設 Clojure 工程師年薪的 5 % 會拿來買書,其中的 30 % 則用來購買技術相關書籍,每年花在購買技術書籍的錢,可以用以下公式計算出來:

spending = ((salary * 0.05) * 0.3) * year

改寫成 Clojure 程式碼後,計算十年花費了多少錢:

(defn spending [salary year]
  (* (* (* salary 0.05) 0.3) year))
(spending 10000 10)
;; => 1500.0

看起來運作的不錯,但是計算花費的函式本體一層又一層的計算,真不容易閱讀。首位穿引巨集 (Thread-first macro) 正是解決這個問題的好方法。首位穿引巨集的寫法看起來就像是箭頭,由一個橫線 (-) 加上大於符號 (>) 組成,首位穿引巨集中的第一個參數運算式求值完成後,結果會傳遞給下一個參數運算式的第一個參數。以下是改用首位穿引巨集之後的結果:

(defn spending [salary year]
  (-> (* salary 0.05)
      (* 0.3)
      (* year)))
(spending 10000 10)
;; => 1500.0

一圖勝千言:

+-------------------+
|                   |
|   * salary 0.05   +-----------+
|                   |           |
+-------------------+           |
                      +-----+---v----+-----+
                      |     |        |     |
                      |  *  |        | 0.3 +-----------+
                      |     |        |     |           |
                      +-----+--------+-----+           |
                                             +-----+---v---+------+
                                             |     |       |      |
                                             |  *  |       | year |
                                             |     |       |      |
                                             +-----+-------+------+

修改過後的程式碼變得更容易閱讀。前一個運算式的結果,接上下一個運算式的第一個參數的位置,一目瞭然。

末位穿引

首位穿引巨集是將結果放在下一個運算式的第一個參數,而末位穿引巨集 (Thread-last macro) 則是將結果放在下一個運算式的最後一個參數,寫法爲一個橫線 (-) 加上兩個大於符號 (>)。

修改前的運算式如下所示:

(take 5 (map #(+ % 2) (range 10)))
;; => (2 3 4 5 6)

使用末位穿引巨集改寫之後:

(->> (range 10)
     (map #(+ % 2))
     (take 5))
;; => (2 3 4 5 6)

一圖勝千言:

+-------------+
|             |
|  range  10  +---------------------+
|             |                     |
+-------------+                     |
                +-----+----------+--v--+
                |     |          |     |
                | map | #(+ % 2) |     +---------------+
                |     |          |     |               |
                +-----+----------+-----+               |
                                         +------+---+--v--+
                                         |      |   |     |
                                         | take | 5 |     |
                                         |      |   |     |
                                         +------+---+-----+

回顧

通過本篇文章,你了解了如何使用條件式來決定應該對哪個運算式求值,也了解到達成循環迭代功能的方法;還知道了強大的列表推導,可以生成列表。使用穿引巨集則可以讓程式易於閱讀和理解。

還不賴吧?今天就先到這裡,下一篇文章再見囉!

(本篇文章同步刊登於 GitHub,歡迎在文章下方留言或發送 PR 給予建議與指教)


上一篇
[第 12 天] 擁抱 Clojure:流程控制(二)
下一篇
[第 14 天] 擁抱 Clojure:命名空間與專案(一)
系列文
擁抱 Clojure30

尚未有邦友留言

立即登入留言