Clojure

學習 Clojure - 流程控制

陳述式與表達式

在 Java 中,表達式會傳回值,而陳述式則不會。

// "if" is a statement because it doesn't return a value:
String s;
if (x > 10) {
    s = "greater";
} else {
    s = "less or equal";
}
obj.someMethod(s);

// Ternary operator is an expression; it returns a value:
obj.someMethod(x > 10 ? "greater" : "less or equal");

然而,在 Clojure 中,所有東西都是表達式!所有東西都會傳回值,而多個表達式的區塊會傳回最後一個值。專門執行副作用的表達式會傳回 nil

流程控制表達式

因此,流程控制運算子也是表達式!

流程控制運算子可以組合,因此我們可以在任何地方使用它們。這會減少重複的程式碼,以及減少中間變數。

流程控制運算子也可以透過巨集進行擴充,這允許編譯器透過使用者程式碼進行擴充。我們今天不會討論巨集,但你可以在 巨集從頭開始的 Clojure,或 勇敢而真實的 Clojure 等許多其他地方閱讀更多相關資訊。

if

if 是最重要的條件式,它包含一個條件、「then」和「else」。if 只會評估條件所選的分支。

user=> (str "2 is " (if (even? 2) "even" "odd"))
2 is even

user=> (if (true? false) "impossible!") ;; else is optional
nil

真值

在 Clojure 中,所有值在邏輯上都是真或假。唯一的「假」值是 falsenil,所有其他值在邏輯上都是真的。

user=> (if true :truthy :falsey)
:truthy
user=> (if (Object.) :truthy :falsey) ; objects are true
:truthy
user=> (if [] :truthy :falsey) ; empty collections are true
:truthy
user=> (if 0 :truthy :falsey) ; zero is true
:truthy
user=> (if false :truthy :falsey)
:falsey
user=> (if nil :truthy :falsey)
:falsey

ifdo

if 只為「then」和「else」取用單一表達式。使用 do 來建立較大的區塊,作為單一表達式。

請注意,這樣做的唯一原因是如果你的主體有副作用!(為什麼?)

(if (even? 5)
  (do (println "even")
      true)
  (do (println "odd")
      false))

when

when 是只有 then 分支的 if。它檢查條件,然後評估任意數量的陳述作為主體(因此不需要 do)。最後一個表達式的值會傳回。如果條件為假,則傳回 nil。

when 傳達給讀者,表示沒有「else」分支。

(when (neg? x)
  (throw (RuntimeException. (str "x must be positive: " x))))

cond

cond 是一系列測試和表達式。每個測試依序評估,並為第一個真測試評估並傳回表達式。

(let [x 5]
  (cond
    (< x 2) "x is less than 2"
    (< x 10) "x is less than 10"))

condelse

如果沒有測試滿足,則傳回 nil。一個常見的慣用語是使用 :else 的最後測試。關鍵字(例如 :else)總是評估為真,因此這將始終被選為預設值。

(let [x 11]
  (cond
    (< x 2)  "x is less than 2"
    (< x 10) "x is less than 10"
    :else  "x is greater than or equal to 10"))

case

case 將參數與一系列值進行比較,以尋找匹配項。這是在常數(非線性)時間內完成的!但是,每個值都必須是編譯時間文字(數字、字串、關鍵字等)。

cond 不同,如果沒有值匹配,case 將擲回例外。

user=> (defn foo [x]
         (case x
           5 "x is 5"
           10 "x is 10"))
#'user/foo

user=> (foo 10)
x is 10

user=> (foo 11)
IllegalArgumentException No matching clause: 11

帶有 else 表達式的 case

case 可以有一個最後的尾隨表達式,如果沒有測試匹配,則將評估該表達式。

user=> (defn foo [x]
         (case x
           5 "x is 5"
           10 "x is 10"
           "x isn't 5 or 10"))
#'user/foo

user=> (foo 11)
x isn't 5 or 10

副作用的迭代

dotimes

  • 評估表達式 n

  • 傳回 nil

user=> (dotimes [i 3]
         (println i))
0
1
2
nil

doseq

  • 在序列上進行迭代

  • 如果是一個惰性序列,則強制評估

  • 傳回 nil

user=> (doseq [n (range 3)]
         (println n))
0
1
2
nil

具有多個繫結的 doseq

  • 類似於巢狀 foreach 迴圈

  • 處理序列內容的所有排列組合

  • 傳回 nil

user=> (doseq [letter [:a :b]
               number (range 3)] ; list of 0, 1, 2
         (prn [letter number]))
[:a 0]
[:a 1]
[:a 2]
[:b 0]
[:b 1]
[:b 2]
nil

Clojure 的 for

  • 列表理解,不是 for 迴圈

  • 序列排列組合的產生器函數

  • 繫結的行為類似於 doseq

user=> (for [letter [:a :b]
             number (range 3)] ; list of 0, 1, 2
         [letter number])
([:a 0] [:a 1] [:a 2] [:b 0] [:b 1] [:b 2])

遞迴

遞迴和迭代

  • Clojure 提供 recur 和序列抽象

  • recur 是「經典」遞迴

    • 使用者無法控制它,被視為較低層級的設施

  • 序列將迭代表示為值

    • 使用者可以部分迭代

  • 還原器將迭代表示為函數組合

    • Clojure 1.5 中新增,不在此討論範圍

looprecur

  • 函數式迴圈結構

    • loop 定義繫結

    • recur 以新的繫結重新執行 loop

  • 優先使用高階函式庫函數

(loop [i 0]
  (if (< i 10)
    (recur (inc i))
    i))

defnrecur

  • 函數參數是隱含的 loop 繫結

(defn increase [i]
  (if (< i 10)
    (recur (inc i))
    i))

recur 用於遞迴

  • recur 必須在「尾部位置」

    • 分支中的最後一個運算式

  • recur 必須依位置提供所有繫結符號的值

    • 迴圈繫結

    • defn/fn 參數

  • 透過 recur 進行遞迴不會消耗堆疊

例外

例外處理

  • try/catch/finally 與 Java 中相同

(try
  (/ 2 1)
  (catch ArithmeticException e
    "divide by zero")
  (finally
    (println "cleanup")))

引發例外

(try
  (throw (Exception. "something went wrong"))
  (catch Exception e (.getMessage e)))

包含 Clojure 資料的例外

  • ex-info 接收訊息和映射

  • ex-data 重新取得映射

    • 或者,如果未使用 ex-info 建立,則為 nil

(try
  (throw (ex-info "There was a problem" {:detail 42}))
  (catch Exception e
    (prn (:detail (ex-data e)))))

with-open

(let [f (clojure.java.io/writer "/tmp/new")]
  (try
    (.write f "some text")
    (finally
      (.close f))))

;; Can be written:
(with-open [f (clojure.java.io/writer "/tmp/new")]
  (.write f "some text"))