JohnShen's Blog.

Clojure in Action: 程序结构、程序流程

字数统计: 2.7k阅读时长: 12 min
2019/07/30 Share

程序结构

函数定义

defn宏展开为deffn调用的组合。fn宏接受方括号中的一系列参数然后是程序主体,fn形式可以用于定义匿名函数。

1
2
3
4
5
6
7
(defn addition-function [x y]
(+ x y))

;; Expanded form:
(def addition-function
(fn [x y]
(+ x y)))

let 形式

let形式接受一个向量作为其第一个参数,该向量包含偶数个形式,然后是在 let 求值时进行求值的 0 个或者多个形式。let 形式可以在代码中将一个符号和某个值绑定,从而引入局部命名对象。let 返回的是最后一个表达式的值。

1
2
3
4
5
(let [x 1
y 2
z (+ x y)]
z)
;=> 3

以下函数可以使用 let 将其分成几个部分,使代码清晰。

1
2
3
4
5
6
7
8
9
(defn average-pets []
(/ (apply + (map :number-pets (vals users))) (count users)))


(defn average-pets []
(let [user-data (vals users)
pet-counts (map :number-pets user-data)
total (apply + pet-counts)]
(/ total (count users))))

在 let 中如果不需要关注值,可以使用下划线标识符,下划线标识符本身没有什么特别之处,只是 clojure 的一个惯例。在解构中,下划线标识符更加实用:(let [[_ _ z] [1 2 3]] z)

1
2
3
4
5
6
(defn average-pets []
(let [user-data (vals users)
pet-counts (map :number-pets user-data)
_ (println "total pets:" pet-counts)
total (apply + pet-counts)]
(/ total (count users))))

do

纯函数语言中,程序是没有副作用的,函数的唯一行为就是计算一个值并返回。但是现实世界中必然充满了状态,也必然有副作用,例如向控制台或者日志文件中打印某些内容、在数据库中保存内容就是改变世界状态的副作用。为了将多个表达式转为一个形式,clojure 提供了 do形式。

1
2
3
4
5
(if (is-something-true?)
(do
(log-message "in true branch")
(store-something-in-db)
(return-useful-value)))
1
(for [i (range 1 3)] (do (println i) i))

程序流程

条件

if

(if test consequent alternative)

if形式接受一个测试表达式,若为真则求后续表达式(consequent),若为假则则使用替代形式(alternative)。形式可以结合do形式使其完成多项工作。

1
2
(if (> 5 2)  "yes"  "no")
;=> "yes"

if-not

1
2
(if-not (> 5 2) "yes" "no")
;=> "no"

cond

cond可以将嵌套的if条件数扁平化,(cond & clauses)

1
2
3
4
5
6
(def x 1)
(cond
(> x 0) "greater!"
(= x 0) "zero!"
:default "lesser!")
;=> "greater!"

子句(clauses)是成对的表达式。当一个表达式返回 true,求值相关的后续表达式并返回。 如果所有表达式都没有返回真值,则可以传入取真值的表达式(比如关键词 :default),然后求值相关的后续表达式并返回。

when

when宏是将一个if和一个隐式的do

1
2
3
4
5
6
7
8
9
(when (> 5 2)
(println "five")
(println "is")
(println "greater")
"done")
;five
;is
;greater
;=> "done"

此处就没有必要将do包装这三个函数了,when宏会负责这项工作。

when-not

1
2
3
4
5
6
7
8
9
(when-not (< 5 2)
(println "two")
(println "is")
(println "smaller")
"done")
;two
;is
;smaller
;=> "done"

逻辑函数

and

接受 0 个或多个形式,按顺序求值每个形式,如果任何一个返回 nil 或者 false,则返回该值。如果所有形式都不返回 false 或 nil,则 and 返回最后一个形式的值。如果没有任何值,则返回 true。

1
2
3
4
5
6
7
8
9
10
(and)
;=> true
(and :a :b :c)
;=> :c
(and :a nil :c)
;=> nil
(and :a false :c)
;=> false
(and 0 "")
;=> ""

or

接受 0 个或多个形式并逐一求值,如果任何形式返回逻辑真值,则将该值返回。如果所有形式都不返回逻辑真值,则返回最后一个值。

1
2
3
4
5
6
7
8
9
10
(or)
;=> nil
(or :a :b :c)
;=> :a
(or :a nil :c)
;=> :a
(or nil false)
;=> false
(or false nil)
;=> nil

not

该函数始终返回true或者false

1
2
3
4
5
6
(not true)
;=> false
(not 1)
;=> false
(not nil)
;=> true

比较函数

<<=>>==有一个额外特性:可以取任意数量的参数。

1
2
3
4
5
; < 可检测是否以升序排列
(< 2 4 6 8)
;=> true
(< 2 4 3 8)
;=> false

= 与 ==

=函数等同于 Java 的 equals,但适用于范围更广的对象,包括 nil、数值、序列。Clojure 中的 ==函数只能用于比较数值。=可以比较任意两个值,但比较三种不同类型的数值时结果不理想。

1
2
3
4
5
6
7
8
(= 1 1N 1/1)
;=> true
(= 0.5 1/2)
;=> false
(= 0.5M 0.5)
;=> false
(= 0.5M 1/2)
;=> false

如果对比不同类型的数值,则可以用==代替,但是所有参数必须是数值。

1
2
3
4
5
6
7
8
9
10
11
12
13
(== 1 1N 1/1)
;=> true
(== 1/2 0.5M 0.5)
;=> true
1.9999999999999999
;=> 2.0
(== 2.0M 1.9999999999999999) ; == 不是对抗浮点精度和舍入问题的银弹
;=> true

_(== :a 1)
;ClassCastException clojure.lang.Keyword cannot be cast to java.lang.Number clojure.lang.Numbers.equiv (Numbers.java:206)
_(== nil 1)
;NullPointerException clojure.lang.Numbers.ops (Numbers.java:961)

如果你预计所有要对比的数据都是数值,且预期有不同类型的数字,则使用==,否则使用=

函数式循环

大部分函数式语言都不支持传统的for循环结构,因为for的典型实现需要改变循环计数器的值。作为替代,它们使用递归和函数应用。

while

while宏与命令式语言类似。

1
2
(while (request-on-queue?)
(handle-request (pop-request-queue)))

loop/recur

Clojure 没有传统的for循环,其循环流程控制是使用looprecur

1
2
3
4
5
(defn fact-loop [n]
(loop [current n fact 1]
(if (= current 1)
fact
(recur (dec current) (* fact current) ))))

loop建立和let形式完全相同的绑定,recur也有两个绑定值(def current)(* fact current),他们在计算之后重新与currentfact绑定。

recur看起来像递归,实际上不适用栈。recur仅能作用于代码尾部,如果企图从任何其他位置使用它,编译器会报错。

doseq 和 dotimes

1
2
3
4
5
6
(defn run-report [user]
(println "Running report for" user))

(defn dispatch-reporting-jobs [all-users]
(doseq [user all-users]
(run-report user)))

上面这个例子中doseq的第一个项是一个新符号,以后将绑定到第二个项(必须是一个序列)中的每一个元素。形式的主体将对序列中的每个元素执行,然后整个形式将返回 nil。

dotimes与之类似,接受一个向量(包含一个符号和一个数值),向量中符号被设置为 0 到 (n-1) 的值,并对每个数值求取主体的值。

1
2
(dotimes [x 5]
(println "X is" x))

将打印数字 0~4,返回 nil。

map 、filter、remove、reduce、for

在前篇文章中的数据结构 — 序列中的“序列转换”一节中已提及。这里做一些补充。

  • map
1
2
3
4
5
6
7
8
9
10
(map inc [0 1 2 3])
;=> (1 2 3 4)

;; map 接受一个函数,其可以有任意多个参数以及相同数量的序列。每个序列为函数提供一个参数。
(map + [0 1 2 3] [0 1 2 3])
;=> (0 2 4 6)

;; 返回值的长度等于最短序列的长度
(map + [0 1 2 3] [0 1 2])
;=> (0 2 4)
  • filter
1
2
3
4
5
(defn non-zero-expenses [expenses]
(let [non-zero? (fn [e] (not (zero? e)))]
(filter non-zero? expenses)))
(non-zero-expenses [-2 -1 0 1 2 3])
;=> (-2 -1 1 2 3)
  • remove

    filter判定保留哪些元素,remove判定抛弃哪些元素。两者刚好相反。

1
2
3
4
(defn non-zero-expenses [expenses]
(remove zero? expenses))
(non-zero-expenses [-2 -1 0 1 2 3])
;=> (-2 -1 1 2 3)
  • reduce & reductions

reduce接受一个函数(有两个参数)和一个数据元素序列。函数参数应用到序列的前两个元素,产生第一个结果,之后使用这个结果和序列的下一个元素再次调用同一个函数。重复此过程直到处理完最后一个元素。

1
2
3
4
5
(defn factorial [n]
(let [numbers (range 1 (+ n 1))]
(reduce * numbers)))
(factorial 5)
;=> 120

reduce只返回最终的规约值,而reductions返回每个中间值组成的序列。

1
2
3
4
5
(defn factorial-steps [n]
(let [numbers (range 1 (+ n 1))]
(reductions * numbers)))
(factorial-steps 5)
;=> (1 2 6 24 120)
  • for

可以使用的限定词::let:when:while

1
2
3
4
5
6
7
8
9
10
11
12
(for [x [0 1 2 3 4 5]
:let [y (* x 3)]
:when (even? y)]
y)
;=> (0 6 12)

(def chessboard-labels
(for [alpha "abcdefgh"
num (range 1 9)]
(str alpha num)))
chessboard-labels
;=> ("a1" "a2" "a3" "a4" "a5" … "h6" "h7" "h8")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(defn prime? [x]
(let [divisors (range 2 (inc (int (Math/sqrt x))))
remainders (map (fn [d] (rem x d)) divisors)]
(not (some zero? remainders))))

(defn primes-less-than [n]
(for [x (range 2 (inc n))
:when (prime? x)]
x))
(primes-less-than 50)
;=> (2 3 5 7 11 13 17 19 23 29 31 37 41 43 47)

(defn pairs-for-primes [n]
(let [z (range 2 (inc n))]
(for [x z y z :when (prime? (+ x y))]
(list x y))))
(pairs-for-primes 5)
;=> ((2 3) (2 5) (3 2) (3 4) (4 3) (5 2))

串行宏

thread-first

1
2
(defn final-amount [principle rate time-periods]
(* (Math/pow (+ 1 (/ rate 100)) time-periods) principle))

以上函数定义不易理解,需要从里往外读。使用thread-first->改写如下。该宏所做的是取第一个参数,将其放在下一个表达式的第二个位置。之所以成为thread-first,是因为它将代码移到下一个形式首个参数的位置。之后,它取得整个结果表达式,并将其移到再下一个表达式的第二个位置。

1
2
3
4
5
6
(defn final-amount-> [principle rate time-periods]
(-> rate
(/ 100)
(+ 1)
(Math/pow time-periods)
(* principle)))

thread-last

thread-last->>在取得第一个表达式结果后,将其移入下一个表达式最后的位置。之后,对所有表达式重复该 过程。

1
2
3
4
5
6
7
8
(defn factorial [n]
(reduce * (range 1 (+ 1 n))))

(defn factorial->> [n]
(->> n
(+ 1)
(range 1)
(reduce *)))

thread-last宏更常见的用途是处理数据元素序列以及使用 map、reduce、filter 这样的高阶函数。这些函数都接受序列作为最后一个元素,所以thread-last宏更为合适。

some->some->>和上述两个宏基本相同,但是如果表达式的任意一步的结果是 nil,则计算结束。

thread-as

thread-asas->相比上两者,更加灵活:你为它提供一个名称,它将把各个连续形式的结果绑定到这个名称,以便下一步使用。

1
2
3
4
(as-> {"a" [1 2 3 4]} <>
(<> "a")
(conj <> 10)
(map inc <>))

该例子展开后如下:

1
2
3
4
5
(let [<> {"a" [1 2 3 4]}
<> (<> "a")
<> (conj <> 10)
<> (map inc <>)]
<>)

条件式串行宏

cond->cond->>除了每个形式都包含一个条件之外,和->以及->>基本相同,如果一个条件为false,则对应的形式将会跳过,但是对下一对形式继续串行求值(cond则是在发现为真的判定后将立刻停止后续成对形式的求值,而cond->会对每个条件求值)。

1
2
3
4
5
6
(let [x 1 y 2]
(cond-> []
(odd? x) (conj "x is odd")
(zero? (rem y 3)) (conj "y is divisible by 3")
(even? y) (conj "y is even")))
;=> ["x is odd" "y is even"]

其等价描述为:

1
2
3
4
5
6
(let [x 1 y 2]
(as-> [] <>
(if (odd? x) (conj <> "x is odd") <>)
(if (zero? (rem y 3)) (conj <> "y is divisible by 3") <>)
(if (even? y) (conj <> "y is even") <>)))
;=> ["x is odd" "y is even"]
CATALOG
  1. 1. 程序结构
    1. 1.1. 函数定义
    2. 1.2. let 形式
  2. 2. do
  3. 3. 程序流程
    1. 3.1. 条件
      1. 3.1.1. if
      2. 3.1.2. if-not
      3. 3.1.3. cond
      4. 3.1.4. when
      5. 3.1.5. when-not
    2. 3.2. 逻辑函数
      1. 3.2.1. and
      2. 3.2.2. or
      3. 3.2.3. not
      4. 3.2.4. 比较函数
      5. 3.2.5. = 与 ==
    3. 3.3. 函数式循环
      1. 3.3.1. while
      2. 3.3.2. loop/recur
      3. 3.3.3. doseq 和 dotimes
      4. 3.3.4. map 、filter、remove、reduce、for
    4. 3.4. 串行宏
      1. 3.4.1. thread-first
      2. 3.4.2. thread-last
      3. 3.4.3. thread-as
      4. 3.4.4. 条件式串行宏