Clojure函数式编程
高阶函数
常用高级函数
map映射
map
函数把函数应用到参数列表。
lower-case
函数只有一个参数,要映射到一个列表上:
(map clojure.string/lower-case ["Java" "Imperative" "Weeping"]) ;= ("java" "imperative" "weeping")
乘法操作要有两个参数,所以要映射到两个列表上。注意两个列表的长度要一样:
(map * [1 2 3 4 5] [6 7 8 9 10]) ;= (6 14 24 36 50)
reduce 归约
在没有初始值的情况下,从头两个元素开始:
(reduce max [0 -3 10 48]) ;= 48
有初始值的情况下,从初始值和第一个元素开始:
(reduce + 50 [1 2 3 4]) ;= 60
apply
对于很多函数式风格的程序来到,到底要调用哪个函数是在程序运行过程中决定的。
对于这种场景,Clojure使用apply
函数把参数应用到函数上:
(apply hash-map [:a 5 :b 6]) ;= {:a 5, :b 6}
直接调用函数与apply
应用的区别:
;; 直接调用时操作是列表的第一个元素 (* 5 3 2 2 ) ;= 60 ;; `apply`是把一个参数列表应用于一个函数 (apply * [5 3 2 2 ]) ;= 60 ;; 这样是错误的 (apply * 5 3 2 2 ) ;= IllegalArgumentException ; Don't know how to create ISeq from: java.lang.Long ; clojure.lang.RT.seqFrom (RT.java:505)
apply
的时候还可以有一部分参数是确定的,还有一部分参数是动态生成的,
放在一个序列里:
(def args [2 -2 10]) ;= #'user/args (apply * 0.5 3 args) ;= -60.0
偏函数
当函数没有应用到完整的参数列表,生成的结果是另一个新的函数:偏函数。
partial
生成偏函数。比如过滤序列,只要字符串,其他的丢弃:
(def only-strings (partial filter string?)) ;; filter要两个参数,这里只给第一个 (only-strings ["a" 5 "b" 6]) ;; 另一个参数现在才给 ;= ("a" "b")
参数装包与解包操作
当apply
的参数小于3个、或是partial
的参数小于4个时,
它们不会对参数列表进行装包解包操作,所以不会对性能有影响。
当拥有更多参数的情况下,装包解包的消耗也不明显。
用偏函数可以处理不定长参数,函数字面量要求形参与实参数量匹配
(以不定长参数的函数map
为例):
(#(map * % %2 %3) [1 2 3] [4 5 6] [7 8 9]) ;= (28 80 162) (#(map *) [1 2 3] [4 5 6] [7 8 9]) ;; 实参无,形参有三个,错误 (#(map * % %2 %3) [1 2 3] [4 5 6]) ;; 实参二个,形参有三个,错误
apply
可以用%&
表示函数字面量接受多余参数:
(#(apply map * %&) [1 2 3] [4 5 6] [7 8 9]) ;= (28 80 162) (#(apply map * %&) [1 2 3]) ;= (1 2 3)
偏函数可以直接处理不定长参数:
((partial map *) [1 2 3] [4 5 6] [7 8 9]) ;= (28 80 162)
函数的组合
comp
把操作组合起来,操作执行的顺序 从右到左 。
例如以下代码把多个数字加起来,再取负值,再转为字符串:
(defn negated-sum-str [& numbers] (str (- (apply + numbers))))
可以简单地写为:
(def negated-sum-str (comp str - +))
另一个例子:
(require '[clojure.string :as str]) ;; 引入clojure.string,别名str ;; 把驼峰标记法改为`-`分隔法的关键字 (def camel->keyword (comp keyword ;; 转为关键字类型 str/join ;; 连接为字符串 (partial interpose \-) ;; 用`-`分隔的序列 (partial map str/lower-case) ;; 大写改小写 #(str/split % #"(?<=[a-z])(?=[A-Z])"))) ;; 把驼峰标记法切开 (camel->keyword "CamelCase") ;= :camel-case (camel->keyword "lowerCamelCase") ;= :lower-camel-case
注意上面的interpose
和map
是用的偏函数,因为参数不完整,
有部分参数是要前一个函数的返回值。
->
宏和->>
宏可以实现相同的功能,它们不是函数是宏,会调整传递给它们的代码:
-
->
把它的第一个参数作为后续所有函数的第一个参数。 -
->>
把它的第一个参数作为后续所有函数的最后一个参数。
下面的代码实现了相同的camel->keyword
:
(defn camel->keyword [s] (->> (str/split s #"(?<=[a-z])(?=[A-Z])") (map str/lower-case) (interpose \-) str/join keyword))
comp
和partial
被广泛用于「无参数风格编程」(又称「默契编程」),
特点是定义函数时不用显式地定义它的参数。
还可以进一步组合,定义一个函数,把map中驼峰网络字符串的key转为Clojure风格的 关键字:
(def camel-pairs->map (comp (partial apply hash-map) (partial map-indexed (fn [i x] (if (odd? i) x (camel->keyword x)))))) (camel-pairs->map ["CamelCase" 5 "lowerCamelCase" 3]) ;= {:camel-case 5, :lower-camel-case 3}
缓存结果
memoize
以一个函数为参数,并返回这个函数的内存化版本。
检查质数的函数prime
会花费大量的时间:
(defn prime? [n] (cond (== 1 n) false (== 2 n) true (even? n) false :else (->> (range 3 (inc (Math/sqrt n)) 2) (filter #(zero? (rem n %))) empty?))) (time (prime? 1125899906842679)) ; "Elapsed time: 2181.014 msecs" ;= true
内存化以后,在第二次调用的时间明显缩短:
(let [m-prime? (memoize prime?)] (time (m-prime? 1125899906842679)) (time (m-prime? 1125899906842679))) ; "Elapsed time: 2085.029 msecs" ; "Elapsed time: 0.042 msecs" ;= true
缓存会占用内存:
- 不要把它们定义为顶层函数,而是放在内部,只在用到的时候调用它。
-
使用优化过的
core.memoize
,它会做清除超时缓存等优化操作。