iT邦幫忙

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

擁抱 Clojure系列 第 12

[第 12 天] 擁抱 Clojure:流程控制(二)

流程控制(二)

迭代

由於 Clojure 中的資料結構都是不可變 (Immutable),所以沒有主流程式語言的 for 迴圈,因爲 for 迴圈需要在每次迭代 (Iteration) 修改變數以達到迭代的功能。透過遞迴與函式也能夠做到迴圈的功能。

doseq

如果想要遍歷序列,在每次取得序列中的元素時,就執行一次程式,可以使用 doseqdoseq 接受一個向量跟運算式本體,向量中以類似 let 方式命名一個符號,每次循環繫結序列中的元素:

(doseq [x [1 2 3]]
  (println x))
;; => 1
;; => 2
;; => 3
;; => nil

每次迭代時,符號 x 繫結了元素的內容。

doseq 在遍歷元素時還可設定條件修飾子 (Modifier),決定何時對遍歷時的運算式求值、設定迭代的終止條件、以及在每次遍歷中繫結符號作爲使用:

(doseq [x (range 5)
        y [10 20 30]
        :let [z (* x y)]
        :when (odd? x)]
  (println x y z))
;; => 1 10 10
;; => 1 20 20
;; => 1 30 30
;; => 3 10 30
;; => 3 20 60
;; => 3 30 90
;; => nil

以上範例中,x 爲 0 到 4,y 爲 10 20 30,每次迭代還會設定符號 zxy 相乘的值。迭代時當 x 爲奇數才會運行 (println x y z)

以下範例則示範使用 :while 關鍵字,設定迭代停止條件。當 y 大於或等於 30 時,迭代便終止 :

(doseq [x (range 99)
        :let [y (* x x)] 
        :while (< y 30)]
  (println [x y]))
;; => [0 0]
;; => [1 1]
;; => [2 4]
;; => [3 9]
;; => [4 16]
;; => [5 25]
;; => nil

dotimes

dotimesdoseq 類似,第一個參數的向量中包含符號與數字,數字代表迭代的次數,符號則繫結了每次迭代的次數。迭代的次數爲 0 到 (n - 1)。

(dotimes [x 3]
  (println x))
;; => 0
;; => 1
;; => 2
;; => nil

while

while 非常類似於在其他命令式 (Imperative) 程式語言的兄弟,例如 Ruby 或 Java。while 接受一個測試運算式與本體運算式,當此運算式求值結果爲假時,才會終止對本體運算求值的循環。

(def x (atom 5))
(while (> @x 0)
  (do
    (println @x)
    (swap! x dec)))
;; => 5
;; => 4
;; => 3
;; => 2
;; => 1
nil

以上範例中的 x 爲 Clojure 引用型態 (Reference type) 中的原子 (Atom) 型態,使用 atom 與參數建立原子型態,內容爲 5。若是想要取用原子型態中的資料,須使用 @deref 才可取用到內容。引用型態將會在後續文章詳細解說。

範例的內容爲每次迭代都會檢查 x 是否大於 0,若否則印出其值並將 x 指向的值遞減。

loop/recur

以上提到的控制迭代循環的 doseqdotimes 以及 while 都是利用 Clojure 迭代的基本元素 looprecur 來達成。loop 特殊形式 (Special form) 利用類似 let 語法來繫結循環時會用到的符號與資料的對應,之後包含了循環時運行的本體運算式,recur 運算式則設立下次循環時使用到的新值。以下範例使用 looprecur 示範倒數的功能:

(loop [n 5]                ; 1
  (if (zero? n)
    n                      ; 2
    (do
      (println n)
      (recur (dec n)))))   ; 3
;; => 5
;; => 4
;; => 3
;; => 2
;; => 1
;; => 0
  1. loop 在此建立了 n 的資料繫結,內容初始爲 5。

  2. 此處設立了當到達終止條件 n 爲零時,將執行的運算式。loop 的返回值即爲此運算式求值後的結果。

  3. 最後在此處 recur 設立了下次循環時,loop 已建立的符號 n 的新值,在這裡爲 n - 1。設定完成後,n 將會以新的值從 1 處重新開始循環。

由於 JVM 缺乏尾遞迴 (Tail Recursion) 的最佳化功能,以及遞迴時會消耗記憶體的問題,Clojure 中的使用慣例是利用 looprecur 達成迭代的功能。

recur 除了可以返回 loop 設立的遞迴點 (Recursion point) 之外,也可以返回由函式定義產生的遞迴點。以下範例使用 defnrecur 來實作倒數功能.

(defn countdown [x]
  (if (zero? x)
    x
    (do
      (println x)
      (recur (dec x)))))
;; => #'user/countdown
(countdown 3)
;; => 3
;; => 2
;; => 1
;; => 0

(未完待續)


上一篇
[第 11 天] 擁抱 Clojure:流程控制(一)
下一篇
[第 13 天] 擁抱 Clojure:流程控制(三)
系列文
擁抱 Clojure30

尚未有邦友留言

立即登入留言