Jade Dungeon

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

注意上面的interposemap是用的偏函数,因为参数不完整, 有部分参数是要前一个函数的返回值。

->宏和->>宏可以实现相同的功能,它们不是函数是宏,会调整传递给它们的代码:

  • ->把它的第一个参数作为后续所有函数的第一个参数。
  • ->>把它的第一个参数作为后续所有函数的最后一个参数。

下面的代码实现了相同的camel->keyword

(defn camel->keyword [s]
  (->> (str/split s #"(?<=[a-z])(?=[A-Z])")
    (map str/lower-case)
    (interpose \-)
    str/join
    keyword))

comppartial被广泛用于「无参数风格编程」(又称「默契编程」), 特点是定义函数时不用显式地定义它的参数。

还可以进一步组合,定义一个函数,把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,它会做清除超时缓存等优化操作。