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

caseelse 表达式

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 进行的递归不会消耗堆栈

异常

异常处理

  • 与 Java 中一样,使用 try/catch/finally

(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"))