七週七並分發之函數式編程

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,並不理想,緣由仍是逐頁的進行計數合併java

  • 利用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對象賦值,以前建立的線程就再也不阻塞了
相關文章
相關標籤/搜索