destructure in clojure

Clojure是一个漂亮优雅的语言,就像是一个美丽的少女,让我着迷

解构(destructure)就是一个让Clojure变得漂亮的一个特性,使用解构可以写出极为简洁优雅的代码

什么是解构

Clojure 支持抽象数据结构绑定,通常也称作解构,这在let绑定,函数参数绑定,或者能展开为let绑定或者函数绑定的宏中都是非常常见的用法

Destructure Vector

一个解构最简单的例子就是对一个向量赋值

1
2
3
4
5
6
user=> (def point [5 7])
#'user/point

user=> (let [[x y] point]
(println "x:" x "y:" y))
x: 5 y: 7

解构还可以把某一些现在不关心的值都先放在一起处理,可以只是先把前几个关心的值解构出来

1
2
3
4
5
6
user=> (def indexes [1 2 3])
#'user/indexes

user=> (let [[x & more] indexes]
(println "x:" x "more:" more))
x: 1 more: (2 3)

我们还可以使用:as来绑定整一个向量

1
2
3
4
5
6
user=> (def indexes [1 2 3])
#'user/indexes

user=> (let [[x & more :as full-list] indexes]
(println "x:" x "more:" more "full list:" full-list))
x: 1 more: (2 3) full list: [1 2 3]

Destructure Map

解构向量只是很简单的一部分,最常用的还是用来解构映射表(map)

1
2
3
4
5
6
user=> (def point {:x 5 :y 7})
#'user/point

user=> (let [{the-x :x the-y :y} point]
(println "x:" the-x "y:" the-y))
x: 5 y: 7

当然我们也可以去掉上面那个例子中let内部局部绑定的名字

1
2
3
4
5
6
user=> (def point {:x 5 :y 7})
#'user/point

user=> (let [{x :x y :y} point]
(println "x:" x "y:" y))
x: 5 y: 7

但是假如你需要解构的键超过两个,甚至十多个,而且假如键的长度不止一个字符,那么像上面那样写岂不是很蛋疼,所以Clojure还提供了一种更优雅的解决方案,可以减少一半的工作量

1
2
3
4
5
6
user=> (def point {:x 5 :y 7})
#'user/point

user=> (let [{:keys [x y]} point]
(println "x:" x "y:" y))
x: 5 y: 7

所以可以看到这种初看很怪异的解构写法,和之前的例子是类似的功能,只不过可以让我们不需要把重复的名字敲两遍

同样在解构map的时候也可以像解构vector一样,通过使用:as从而得到整一个要解构的map

1
2
3
4
5
6
user=> (def point {:x 5 :y 7})
#'user/point

user=> (let [{:keys [x y] :as the-point} point]
(println "x:" x "y:" y "point:" the-point))
x: 5 y: 7 point: {:x 5, :y 7}

:as对应的是,我们可以使用:or来设置解构的默认值,也就是说如果传入的map没有对应的解构值,那么我们在上下文中就使用:or指定的默认值

1
2
3
4
5
6
user=> (def point {:y 7})
#'user/point

user=> (let [{:keys [x y] :or {x 0 y 0}} point]
(println "x:" x "y:" y))
x: 0 y: 7

同样,你也可以使用解构来拆解嵌套的map结构

1
2
3
4
5
6
user=> (def book {:name "SICP" :details {:pages 657 :isbn-10 "0262011530"}})
#'user/book

user=> (let [{name :name {:keys [pages isbn-10]} :details} book]
(println "name:" name "pages:" pages "isbn-10:" isbn-10))
name: SICP pages: 657 isbn-10: 0262011530

mapvector在Clojure内部都是一样的抽象数据结构Sequence,都是序列,所以一般mapvector的操作都是类似的,所以我们也可以解构一个嵌套的vector

1
2
3
4
5
6
user=> (def numbers [[1 2][3 4]])
#'user/numbers

user=> (let [[[a b][c d]] numbers]
(println "a:" a "b:" b "c:" c "d:" d))
a: 1 b: 2 c: 3 d: 4

当然如果是mapvector嵌套在一起了,也可以轻松解构

1
2
3
4
5
6
user=> (def golfer {:name "Jim" :scores [3 5 4 5]})
#'user/golfer

user=> (let [{name :name [hole1 hole2] :scores} golfer]
(println "name:" name "hole1:" hole1 "hole2:" hole2))
name: Jim hole1: 3 hole2: 5

Destructure in Function

Clojure函数中参数传递时其实就是使用了隐式的let绑定,所以上面提到的所有解构技巧,都可以使用在Clojure函数的参数传递上

我们可以将上文中最后一个例子应用到函数传参中

1
2
3
4
5
6
user=> (defn print-status [{name :name [hole1 hole2] :scores}] 
(println "name:" name "hole1:" hole1 "hole2:" hole2))
#'user/print-status

user=> (print-status {:name "Jim" :scores [3 5 4 5]})
name: Jim hole1: 3 hole2: 5

再看一些其他的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
;; Return the first element of a collection
(defn my-first
[[first-thing]] ; Notice that first-thing is within a vector
first-thing)

(my-first ["oven" "bike" "waraxe"])
; => "oven"

(defn chooser
[[first-choice second-choice & unimportant-choices]]
(println (str "Your first choice is: " first-choice))
(println (str "Your second choice is: " second-choice))
(println (str "We're ignoring the rest of your choices. "
"Here they are in case you need to cry over them: "
(clojure.string/join ", " unimportant-choices))))
(chooser ["Marmalade", "Handsome Jack", "Pigpen", "Aquaman"])
; =>
; Your first choice is: Marmalade
; Your second choice is: Handsome Jack
; We're ignoring the rest of your choices. Here they are in case \
; you need to cry over them: Pigpen, Aquaman

(defn announce-treasure-location
[{lat :lat lng :lng}]
(println (str "Treasure lat: " lat))
(println (str "Treasure lng: " lng)))
(announce-treasure-location {:lat 28.22 :lng 81.33})
; =>
; Treasure lat: 28.22
; Treasure lng: 81.33

;; Works the same as above.
(defn announce-treasure-location
[{:keys [lat lng]}]
(println (str "Treasure lat: " lat))
(println (str "Treasure lng: " lng)))

;; Works the same as above.
(defn receive-treasure-location
[{:keys [lat lng] :as treasure-location}]
(println (str "Treasure lat: " lat))
(println (str "Treasure lng: " lng))

;; One would assume that this would put in new coordinates for your ship
(println treasure-location))

(receive-treasure-location {:lat 3 :lng 33})
; =>
; Treasure lat: 3
; Treasure lng: 33
; {:lat 3, :lng 33}

更多细节可以参考官方文档special forms