JohnShen's Blog.

Clojure in Action: Clojure 构件

字数统计: 1.9k阅读时长: 8 min
2019/07/31 Share

元数据

元数据提供了在必要时为值添加标识的 一种手段。

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
(def untrusted (with-meta {:command "delete-table" :subject "users"}
{:safe false :io true}))

; 可以使用读取器宏^{}简化元数据定义
(def untrusted ^{:safe false :io true} {:command "delete-table"
:subject "users"})
untrusted
;=> {:command "delete-table", :subject "users"}

;检查与值关联的元数据,可以使用meta函数
(meta untrusted)
;=> {:safe false, :io true}

;元数据不影响值的相等性。
(def trusted {:command "delete-table" :subject "users"})
(= trusted untrusted)
;=> true

;元数据在读取时可以加入,而不可以在求值时加入。下列程序将元数据与以hash-map开头的列表关联,而不是与函数调用产生的哈希映射关联,所以这个元数据在运行时不可见。
(def untrusted2 ^{:safe false :io true} (hash-map :command "delete-table"
:subject "users"))
(meta untrusted2)
;=> nil

; 从有元数据的新值中创建新值时,元数据被复制到新数据里
(def still-untrusted (assoc untrusted :complete? false))
still-untrusted
;=> {:complete? false, :command "delete-table", :subject "users"}
(meta still-untrusted)
;=> {:safe false, :io true}

函数与宏也可以在定义中包含元数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(defn ^{:safe true :console true
:doc "testing metadata for functions"}
testing-meta
[]
(println "Hello from meta!"))

(meta testing-meta)
;=> nil

(meta (var testing-meta))
;=> {:ns #<Namespace user>,
; :name testing-meta,
; :file "NO_SOURCE_FILE",
; :line 1, :arglists ([]),
; :console true,
; :safe true,
; :doc "testing metadata for functions"}

Java 类型提示

调用Java方法时,需要通过类找到方法的实现。但是,Clojure 是动态语言,变量类型只有在运行时才知道。可以使用读取器宏^symbol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(set! *warn-on-reflection* true) ; Warn us when reflection is needed.

(defn string-length [x] (.length x))
(time (reduce + (map string-length (repeat 10000 "12345"))))
;Reflection warning
;"Elapsed time: 45.751 msecs"
;=> 50000

(defn fast-string-length [^String x] (.length x)) ; No reflection warning.
(time (reduce + (map fast-string-length (repeat 10000 "12345"))))
;"Elapsed time: 5.788 msecs"
;=> 50000

(meta (first (first (:arglists (meta #'fast-string-length)))))
=> {:tag String}

Clojure 编译器在类型推导上相当智能,所有核心函数已经在必要时做了类型提示,所以不经常需要采用类型提示。

原始类型没有可读的类名可提供引用,Clojure 为所有原始类型和原始类型数组定义了别名:只需要使用^byte这样的类型提示表示原始类型,^bytes这样的复数形式表示原始类型数组。

Java 异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(defn average [numbers]
(let [total (apply + numbers)]
(/ total (count numbers))))
(average [])
;ArithmeticException Divide by zero clojure.lang.Numbers.divide (Numbers.java:156)

(defn safe-average [numbers]
(let [total (apply + numbers)]
(try
(/ total (count numbers))
(catch ArithmeticException e
(println "Divided by zero!")
0))))
(safe-average [])
;Divided by zero!
;=> 0

如果有表达式产生异常,则 根据异常类型执行对应的 catch子句,返回该子句的值。可选的finally子句总会被执行,用于保证必须的副作用,但不返回任何数值。

1
2
3
4
5
6
7
(try
(print "Attempting division... ")
(/ 1 0)
(finally
(println "done.")))
Attempting division... done.
;=>Execution error (ArithmeticException) at user/eval1516 (form-init4016394056826807004.clj:3).Divide by zero

需要注意catch子句的顺序。

1
2
3
4
5
6
7
8
9
10
(try
(print "Attempting division... ")
(/ 1 0)
(catch RuntimeException e "Runtime exception!")
(catch ArithmeticException e "DIVIDE BY ZERO!")
(catch Throwable e "Unknown exception encountered!")
(finally
(println "done.")))
;Attempting division... done.
;=> "Runtime exception!"

异常可以使用throw形式抛出,在希望抛出的场合可以使用: (throw (Exception. "this is an error"))

函数

先决和后置条件

在执行函数主体之前运行的检查由:pre指定,称为先决条件:post键指定的条件称为后置条件,条件中的%指的就是函数的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(defn item-total [price quantity discount-percentage]
{:pre [(> price 0) (> quantity 0)]
:post [(> % 0)]}
(->> (/ discount-percentage 100)
(- 1)
(* price quantity)
float))


(item-total 100 2 0)
;=> 200.0
(item-total 100 2 10)
;=> 180.0

(item-total 100 -2 10)
;Execution error (AssertionError) at user/item-total (form-init4016394056826807004.clj:1).
;Assert failed: (> quantity 0)
(item-total 100 2 110)
;Execution error (AssertionError) at user/item-total (form-init4016394056826807004.clj:1).
;Assert failed: (> % 0)

重载(多种参数数量)

1
2
3
4
5
(defn total-cost
([item-cost number-of-items]
(* item-cost number-of-items))
([item-cost]
(total-cost item-cost 1)))

可以从某种参数数量的函数中调用其他参数数量的版本。

可变参数函数

在 Clojure 中使用&符号实现变长参数功能。

1
2
(defn total-all-numbers [& numbers]
(apply + numbers))

可变参数函数中有一些不可变的参数。可变参数中的必要参数数量至少要与最长的固定参数相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(defn many-arities
([] 0)
([a] 1)
([a b c] 3)
([a b c & more] "variadic"))

(many-arities)
;=> 0
(many-arities "one argument")
;=> 1
(many-arities "two" "arguments")
;Execution error (ArityException) at user/eval1554 (form-init4016394056826807004.clj:11).
;Wrong number of args (2) passed to: user/many-arities
(many-arities "three" "argu-" "ments")
;=> 3
(many-arities "many" "more" "argu-" "ments")
;=> "variadic"

高阶参数

every?

接受一个返回布尔值的函数(判定函数)和一个序列

1
2
3
4
5
(def bools [true true true false false])
(every? true? bools)
;=> false
(every? even? '(2 4 6))
;=> true

some

接受一个判定和一个序列,返回获得的第一个逻辑true值,如果调用都不返回逻辑 true,则返回 nil。

1
2
(some (fn [p] (= "rob" p)) ["kyle" "siva" "rob" "celeste"])
;=> true

constantly

接受一个值 v,返回一个可变参数函数,这个函数不管输入的参数为何,总是返回相同的值 v。

1
2
3
4
5
6
7
8
(def two (constantly 2)) ; same as 
;(def two (fn [& more] 2))
;(defn two [& more] 2)
;=> #'clj-in-act.ch3/two
(two 1)
;=> 2
(two :a :b :c)
;=> 2

complement

接受一个函数作为参数,返回与原始函数参数数量相同、完成相同工作但返回逻辑相反值的函数。

1
2
3
4
5
6
7
8
9
10
11
12
(defn greater? [x y]
(> x y))
(greater? 10 5)
;=> true
(greater? 10 20)
;=> false

(def smaller? (complement greater?))
(smaller? 10 5)
;=> false
(smaller? 10 20)
;=> true

comp

接受多个函数并返回由哪些函数组合而成的新函数。计算从右到左进行,新函数将其参数应用于原始组成函数中最右侧的一个,然后将结果应用到它左边的函数,直到所有函数都被调用。

1
2
3
4
5
6
7
(def opp-zero-str (comp str not zero?))
; (defn opp-zero-str [x] (str (not (zero? x))))

(opp-zero-str 0)
;=> "false"
(opp-zero-str 1)
;=> "true"

partial

接受函数 f 以及 f 的几个参数,然后partial返回一个新函数,接受 f 的其余参数。当以余下的参数调用新函数时,它以全部参数调用原始函数 f。

1
2
3
4
5
6
7
(defn above-threshold? [threshold number]
(> number threshold))

(filter (fn [x] (above-threshold? 5 x)) [1 2 3 4 5 6 7 8 9])
;=> (6 7 8 9)
(filter (partial above-threshold? 5) [1 2 3 4 5 6 7 8 9])
;=> (6 7 8 9)

memoize

内存化可以避免函数为已处理过的参数计算结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(defn slow-calc [n m]
(Thread/sleep 1000)
(* n m))
(time (slow-calc 5 7))
;"Elapsed time: 1000.097 msecs"
;=> 35

(def fast-calc (memoize slow-calc))
(time (fast-calc 5 7))
;"Elapsed time: 1002.446198 msecs"
;=> 35
(time (fast-calc 5 7))
;"Elapsed time: 0.089624 msecs"
;=> 35

注意:memoize缓存没有限定大小,因而会不停缓存输入和结果。因此该函数只应该用于少量可能输入的函数,否则最终会把内存耗尽。更高级的内存化功能,可以使用clojure.core.memoize库。

匿名函数

匿名函数使用fn创建。

1
2
3
(defn sorter-using [ordering-fn]
(fn [collection]
(sort-by ordering-fn collection)))

同时可以使用#()创建一个匿名函数。%表示一个参数,如果超过一个参数,则可以使用%1%2…还可以使用%&表示除明确引用的%&参数之外的参数。

1
2
3
4
5
6
7
8
(#(vector %&) 1 2 3 4 5)
;=> [(1 2 3 4 5)]
(#(vector % %&) 1 2 3 4 5)
;=> [1 (2 3 4 5)]
(#(vector %1 %2 %&) 1 2 3 4 5)
;=> [1 2 (3 4 5)]
(#(vector %1 %2 %&) 1 2)
;=> [1 2 nil]
CATALOG
  1. 1. 元数据
    1. 1.1. Java 类型提示
  2. 2. Java 异常处理
  3. 3. 函数
    1. 3.1. 先决和后置条件
    2. 3.2. 重载(多种参数数量)
    3. 3.3. 可变参数函数
    4. 3.4. 高阶参数
      1. 3.4.1. every?
      2. 3.4.2. some
      3. 3.4.3. constantly
      4. 3.4.4. complement
      5. 3.4.5. comp
      6. 3.4.6. partial
      7. 3.4.7. memoize
    5. 3.5. 匿名函数