3 函数式编程
3.2 第一天:抛弃可变状态
- java从api无法判断一个方法内部有没有隐藏的可变状态
clojure一点儿语法介绍
- clojure由s-表达式构成 如:(+ 1 (* 2 3))
- 数组使用方括号 (def droids ["a" "b" "c"])
- map字面量用花括号 (def me {:name "a" :age 45 :sex :male})
- 使用def定义常量,defn可以定义函数
- clojure知道操作符的特征值(加法是0,乘法是1) (+ 1 2 3 4 5 5)是20 (+)是0,(*)是1
- map的两个处理函数:get和assoc.get从map中查找键,找到则返回对应值,否则返回默认值.assoc接受一个map和一个键值对,在原有map的基础上返回一个加入了这个键值对的新map
(def counts {"apple" 2 "orange" 1})
=> (get counts "apple" 0) //2
=> (get counts "apples" 0) //0
=> (assoc counts "banana" 1) //{"apple" 2, "orange" 1, "banana" 1}
=> (assoc counts "apple" 3) //{"apple" 3, "orange" 1}注意是没有上面加的banana的哦
- frequencies函数:能针对任何集合输出每个集合中每个元素的出现次数
- 映射函数map(这里map是一个函数,不是定义map啊!)
=> (map inc [0 1 2 3 4 5]) //(1 2 3 4 5 6)
=> (map (fn [x] ( * 2 x )) [0 1 2 3 4 5]) //(0 2 4 6 8 10)
- partial函数:partial接受一个函数和若干参数,返回一个被局部调用代入的参数.利用partial函数可以简化上面第二个调用
=> (def multiply-by-2 ( partial * 2 )) //(multiply-by-2 3)是6
=> (map (partial * 2 ) [0 1 2 3 4 5]) //(0 2 4 6 8 10)
- 可以利用正则表达式将字符串切割成词的序列:
user=> (defn get-words [text] (re-seq #"\w+" text))
user=> (get-words "one two three four")
#("one" "two" "three" "four")
user=> (map get-words ["one two three" "4 5 6" "seven eight nine"])
#(("one" "two" "three") ("4" "5" "6") ("seven" "eight" "nine"))
#如果需要包含所有输出的一维序列,可以使用mapcat
user=> (mapcat get-words ["one two three" "4 5 6" "seven eight nine"])
#("one" "two" "three" "4" "5" "6" "seven" "eight" "nine")
第一个函数式程序
对一个数列求和
- 版本一:
(defn recursive-sum [numbers]
(if (empty? numbers)
0
(+ (first numbers) (recursive-sum (rest numbers)))))
- 简化版本二:这里用了reduce函数,三个参数:一个化简参数,一个初始值,一个集合.代码中用fn定义了一个匿名化简函数,其接受两个参数并返回参数的和.reduce为集合中的每一个元素都调用一次化简参数
(defn reduce-sum [numbers]
(reduce (fn [acc x] (+ acc x)) 0 numbers))
- 简化版本三: +是一个现成的函数,接受两个参数并返回参数的和
(defn sum [numbers]
(reduce + numbers))
- 版本四: 并行
(ns sum.core (:require [clojure.core.reducers :as r]))
(defn parallel-sum [numbers]
(r/fold + numbers))
懒惰一点好
- clojure中序列是懒惰的 user=>user=> (take 10 (range 0 100000000000000000000000))可以马上输出,由于take只需要前10个元素,所以range只需要产生十个元素
- 设置可以使用无穷序列,比如iterate会不断将某个函数应用到初始值,第一次的返回值,第二次的返回值....来构成无穷序列
user=> (take 10 (iterate inc 0))
user=> (take 10 (iterate (partial + 2) 2))
# (2 4 6 8 10 12 14 16 18 20)
- 所谓序列是懒惰的,不仅意味着其仅在需要时才生成序列的尾元素,还意味着使用过的元素在使用后,如果不再使用,会被舍弃 例如user=>(take-last 5 (range 0 100000000))可能会运行很久,但是不会耗尽内存.
3.3 函数式并行
- pmap功能类似于map,区别是应用pmap的过程可以是并行的,pmap在需要结果的时候可以并行计算,但仅生成需要的而不是全部的结果
- 读取器宏#(...)来声明传给pmap的函数,读取器宏可以快速的创建匿名函数,函数的参数通过%1,%2来标示,如果只有一个参数,%
- 例如#( frequencies ( get-words %) ) 等价于 ( fn [page] (frequencies (get-words page ) ) )
每次一页
user=> (def pages ["one potato two potato three potato four"
"five potato six potato seven potato more"])
user=> (defn get-words [text] (re-seq #"\w+" text))
user=> (pmap #(frequencies (get-words %)) pages)
# ({"one" 1, "potato" 3, "two" 1, "three" 1, "four" 1} {"five" 1, "potato" 3, "six" 1, "seven" 1, "more" 1})
化简函数
- 将所有所得的序列简化成一个map,从而得到词频总数.按照一定规则合并两个map API: (merge-with f & maps) 这个api说明有两个参数 一个函数 以及数个maps user=> (merge-with + {:x 1 :y 2} {:y 1 :z 3}) #{:x 1, :y 3, :z 3}
并行版本的词频统计程序
(defn count-words-parallel [pages]
(reduce (partial merge-with +)
(pmap #(frequencies (get-words %)) pages)))
# 使用
user=> (count-words-parallel pages)
{"one" 1, "potato" 6, "two" 1, "three" 1, "four" 1, "five" 1, "six" 1, "seven" 1, "more" 1}
利用批处理改善性能
-
上面的程序加速比1.5,并不理想,原因还是逐页的进行计数合并
-
利用partition-all函数,可以将一个序列中的元素分批,构成多个序列 eg.
user=> (partition-all 4 [1 2 3 4 5 6 7 8 9 10])
((1 2 3 4) (5 6 7 8) (9 10))
- 利用partition-all函数改写word-count
(defn count-words [pages]
(reduce (partial merge-with +)
(pmap count-words-sequential (partition-all 100 pages))))
化简器(reducer)
- 一个化简器,并不代表函数返回的结果,而是代表如何产生记过的描述,被传给reduce或fold之前,化简器不会进行求值,这样做有两个好处: 1.嵌套的函数返回化简器比返回懒惰序列的效率更高,他不用构造处于中间状态的序列 2.对整个嵌套链的集合操作,可以用fold进行并行化
- 普通的map和clojure.core.reducers提供的map不同,前一个返回结果序列,后一个返回的是一个化简器reducible:
user=>user=> (require '[clojure.core.reducers :as r])
nil
user=> (r/map (partial * 2) [1 2 3 4])
#object[clojure.core.reducers$folder$reify__107 0x648c94da "clojure.core.reducers$folder$reify__107@648c94da"]
- reducible不能作为值被直接使用,而是作为参数传给reduce
user=> (reduce conj [] (r/map (partial * 2) [1 2 3 4])) #[2 4 6 8]
- conj第一个参数是一个集合(初始时是空集合[]) 其将第二个参数合并到第一个参数中.因此这个代码和只执行map的结果相同.into函数内部使用了reduce,所以下面代码和上面的等价
user=> (into [] (r/map (partial * 2) [1 2 3 4])) #[2 4 6 8]
- clojure.core提供的大部分序列处理函数都有对应的化简器版本,包括之前见过的map和mapcat.
3.4 第三天: 函数式并发
- 前两天一直在关注并行,今天注意力转向并发
- 函数式语言有一种声明式的范儿.函数式程序并不是描述"如何求值以得到结果",而是描述"结果应当是怎样的".因此,在函数式编程中,如何安排求值顺序来获得最终结果是相对自由的,这正是函数式代码可以轻松并行的关键所在.
引用透明性
在纯粹的函数式语言中,每个函数都具有引用透明性-在任何调用函数的地方,都可以调用函数运行的结果来替换函数的调用,而不会对程序产生副作用.
数据流式编程(dataflow programming)
- 所有函数理论上都可以同时执行.clojure提供了future模型和promise模型来支持这种执行方式
Future模型
- future函数可以接受一段代码,并在一个单独的线程中执行这段代码.其返回一个future对象(所以也是用了返回中间结果的这种方式吗),利用deref或其简写@来解引用
user=> (def sum ( future (+ 1 2 3 4 5 )))
user=> sum
#object[clojure.core$future_call$reify__6962 0x561868a0 {:status :ready, :val 15}]
user=>user=> (deref sum)
15
user=>user=> @sum
15
- 对future对象进行解引用将阻塞当前线程,直到其代表的值(被求值)变得可用.上面的那副数据流图现在可以构造出来了:
user=> (let [a (future (+ 1 2))
b (future (+ 3 4))]
(+ @a @b))
#10
- 这段代码首先用let将(future(+ 1 2))赋值给a,同理给b,对(+ 1 2 ) (+ 3 4 )的求值可以分别在不同线程中进行,最外层的加法将一直阻塞,直到内层的加法完成.
promise模型
- 类似于future对象,promise对象也是异步求值的,也通过defer或者@解引用,在求值前也会阻塞线程,不同的是创建一个promise对象之后,使用promise对象的代码不会立即执行,而是会等到用deliver为promise对象赋值之后才会执行
user=> (def meaning-of-life (promise))
user=> (future (println "the meaning of life is:" @meaning-of-life))
#object[clojure.core$future_call$reify__6962 0x6a55299e {:status :pending, :val nil}]
user=>user=> (deliver meaning-of-life 42)
the meaning of life is: 42
#object[clojure.core$promise$reify__7005 0x289710d9 {:status :ready, :val 42}]
- 这样用future函数创建线程是clojure的惯例,最后用deliver为promise对象赋值,之前创建的线程就不再阻塞了