Clojure
零基礎(chǔ)
學(xué)習(xí)筆記
偏函數(shù)
串行宏
高階函數(shù)
閉包
函數(shù)組合 --- 簡單而又有力的武器
在函數(shù)式編程中茄袖,我們偏愛使用不可變值操软,聲明式的處理,以及函數(shù)組合來解決問題宪祥。我們已經(jīng)在之前的章節(jié)里簡要介紹了不可變值以及聲明式遍歷聂薪,而函數(shù)組合,其實早在一開始學(xué)會如何使用函數(shù)的時候蝗羊,就已經(jīng)開始運用這種技巧了藏澳。
提起可組合性,你可能會想到面向?qū)ο缶幊讨邪岩粋€對象包裹在另一個對象中的行為耀找,也許腦海中還會蹦出幾個設(shè)計模式的名字翔悠,或者你對其一無所知腦中一片空白。不過這都不重要野芒,暫時忘記之前你所了解到的繁瑣的面向?qū)ο缶幊讨械慕M合蓄愁,這次我們所介紹的函數(shù)組合簡單易用又強大。
何為函數(shù)組合复罐?顧名思義涝登,就是把多個函數(shù)擰巴在一起形成一個新函數(shù)。
Clojure 提供了許多工具來幫助你進(jìn)行函數(shù)組合效诅。
comp
使用 comp
函數(shù)(也就是英文 composition 的前四個字母)可以把幾個函數(shù)組合成一個函數(shù)胀滚。
把大象裝進(jìn)冰箱需要三步:
- 打開冰箱
- 把大象塞進(jìn)去
- 關(guān)上冰箱
(def refrigerator {:open? false, :content ["milk", "apple"]}) ;; 冰箱
(defn open-it
[container]
(if (:open? container)
container
(assoc container :open? true)))
(defn close-it
[container]
(if (:open? container)
(assoc container :open? false)
container))
(defn put-in
[container something]
(let [{:keys [open? content]} container]
(if open?
(assoc container :content (conj content something))
container)))
(defn put-elephant-in
[container]
(put-in container "elephant"))
((comp close-it put-elephant-in open-it) refrigerator)
;; 上述表達(dá)式的值為
;= {:open? false, :content [milk apple elephant]}
如果前一個執(zhí)行的函數(shù)的返回值并不能作為后一個函數(shù)的參數(shù),那么在執(zhí)行的時候就會出現(xiàn)問題乱投。
注意咽笼,comp
組合的函數(shù)執(zhí)行順序是從右往左的。
如果不使用 comp
戚炫,那么也可以有下面這樣等價的調(diào)用方式:
(close-it (put-elephant-in (open-it refrigerator)))
這也是為啥 comp
要以看起來很奇怪的從右往左的順序執(zhí)行的原因剑刑。
通常情況下,簡單起見双肤,類似 put-elephant-in
這種用于某一特定情形下只使用一次的函數(shù)施掏,可以使用“匿名函數(shù)的字面量”來簡化它。(函數(shù)字面量在第 8 節(jié) Clojure 學(xué)習(xí)筆記 :8 遍歷元素 中有所介紹茅糜。)
也就是不需要單獨定義它七芭,而是直接在需要的位置填寫函數(shù)字面量:
((comp close-it #(put-in % "elephant") open-it) refrigerator)
;= {:open? false, :content [milk apple elephant]}
為了使之應(yīng)用于更廣泛的行為 --- 把任意東西放進(jìn)冰箱,可以再次將其改寫為一個高階函數(shù):
(defn put-some
[something]
(comp close-it #(put-in % something) open-it)) ;;返回值是一個函數(shù)蔑赘!
然后就可以這么來使用了:
((put-some "elephant") refrigerator)
;= {:open? false, :content [milk apple elephant]}
在第五集中狸驳,我們已經(jīng)簡單接觸了高階函數(shù)预明。所謂高階函數(shù),就是說這個函數(shù)可以接受函數(shù)作為參數(shù)耙箍,或撰糠,返回值是函數(shù)。按照這個說法辩昆,高階函數(shù)其實隨處可見阅酪。比如上面介紹的 comp
自然就屬于高階函數(shù)。
除了使用 Clojure 預(yù)先提供的高階函數(shù)來進(jìn)行函數(shù)組合卤材,我們還可以自己來編寫高階函數(shù)遮斥,比如 put-some
函數(shù)。
串行宏
->
與 ->>
稱為串行宏扇丛,它的功能與 comp
基本一致术吗,如果你不想用 comp
那么可以試試這兩款。
(-> (open-it refrigerator)
(put-elephant-in)
(close-it))
;= {:open? false, :content [milk apple elephant]}
;; 當(dāng)然也可以使用函數(shù)字面量
(-> (open-it refrigerator)
#(put-in % "elephant")
(close-it))
;= {:open? false, :content [milk apple elephant]}
->
(一個減號帆精,一個大于號)较屿,它接受一系列表達(dá)式,并把第一個表達(dá)式的值作為第二個表達(dá)式的第一個參數(shù)卓练,然后求出第二個表達(dá)式的值隘蝎,然后再將這個值作為第三個表達(dá)式的第一個參數(shù)……
這樣說可能并不是很清晰。我們用更形象的方式來描述一下襟企。
(-> (open-it refrigerator) ---? ;; 移動下來
(put-elephant-in ______________ )
(close-it))
;; 等效于
(-> (put-elephant-in (open-it refrigerator))
(close-it))
;; 再次移動
(-> (put-elephant-in (open-it refrigerator)) ┄┄┄┐ ;; 移動下來
┌┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┘
↓
(close-it ___ ))
;; 等效于
(-> (close-it (put-elephant-in (open-it refrigerator))))
;; 無法再繼續(xù)移動嘱么,移動完畢,最終結(jié)果
;; (close-it (put-elephant-in (open-it refrigerator)))
;= {:open? false, :content [milk apple elephant]}
->>
(一個減號顽悼,兩個大于號)曼振,它的效果與 ->
顯著的區(qū)別就是:
->
把上一表達(dá)式的值作為下一表達(dá)式的第一個參數(shù),
而 ->>
是把上一表達(dá)式的值作為下一表達(dá)式的最后一個參數(shù)蔚龙。
你可以這么來記憶冰评,“長箭頭會把內(nèi)容向右推的更遠(yuǎn),而短箭頭力氣比較小木羹,所以只能推到第一個參數(shù)的位置”甲雅。
另外,如果后續(xù)表達(dá)式所需的參數(shù)只有一個(如本例)坑填,也就是無需區(qū)分第一個參數(shù)與最后一個參數(shù)抛人,那么使用兩種串行宏的效果都是一樣的。
而且脐瑰,后續(xù)表達(dá)式無需使用括號括起來函匕,也一樣可以執(zhí)行:
(-> (open-it refrigerator)
put-elephant-in
close-it)
;= {:open? false, :content [milk apple elephant]}
小貼士:
這里所說的 宏 (macro) 并不是函數(shù)。與函數(shù)不同蚪黑,宏會在代碼“運行”之前對代碼做一些“調(diào)整”盅惜,它是 Lisp 的終極武器。
偏函數(shù)
在進(jìn)行函數(shù)組合的時候忌穿,你可能需要固定某個函數(shù)的前幾個參數(shù)值抒寂,比如設(shè)置一些默認(rèn)值,簡化使用掠剑。在 Clojure 中可以使用 partial
來實現(xiàn)這種功能屈芜,稱為偏函數(shù)。
舉個栗子朴译,比如假設(shè)有一個函數(shù)用來訪問服務(wù)器:
(connect-server "8.8.8.8" "53" "data.........")
每次都要輸入服務(wù)器 IP 和 端口是不是太麻煩了井佑?這時候 partial
就派上用場了。
(def connect-googledns (partial connect-server "8.8.8.8" "53"))
(connect-googledns "data1.........")
(connect-googledns "data2.........")
partial
是一個高階函數(shù)眠寿,它效果是構(gòu)造出一個新函數(shù)并返回這個新函數(shù)躬翁,新函數(shù)其實是預(yù)先指定了老函數(shù)開頭的幾個參數(shù)。
你可能會發(fā)現(xiàn)其實函數(shù)字面量或者自己手寫高階函數(shù)也可以實現(xiàn)類似的功能:
(def connect-googledns #(connect-server "8.8.8.8" "53" %))
;; 或者
(defn connect-googledns
[data]
(connect-server "8.8.8.8" "53" data))
而且它們還不限制指定參數(shù)的順序盯拱,partial
卻必須以順序指定參數(shù)盒发。的確是這樣。
但是 partial
的優(yōu)點是不需要了解函數(shù)有多少個參數(shù)狡逢,只指定第一個參數(shù)一樣可以工作:
(partial connect-server "8.8.8.8")
字面量則需要手動填上每個參數(shù)的位置:
#(connect-server "8.8.8.8" %1 %2)
所以宁舰,如果函數(shù)的參數(shù)個數(shù)可變或者個數(shù)比較多,你又想固定開頭的某些參數(shù)奢浑,那么你可以考慮 partial
蛮艰。
閉包
這個概念看起來很神秘。其實在上面的例子中雀彼,我們已經(jīng)使用了閉包壤蚜。
閉包的表象是:一個高階函數(shù) A,它返回一個函數(shù)详羡,而且返回的函數(shù)的某些參數(shù)由 A 來提供仍律。
或者說:某個函數(shù)的參數(shù)由外部作用域提供,而不是自身作用域提供实柠。
也就是說水泉,閉包的不嚴(yán)謹(jǐn)定義就是:某一局部綁定的值在其生存期外依然可以被訪問,因為這個局部綁定被某種東西“包裹”了起來(在 Clojure 中也就是作為函數(shù)參數(shù)窒盐,被函數(shù)包裹起來)草则,然后被作為返回值返回了。這個返回值被其它位置引用蟹漓,所以依然不會被回收炕横。
好吧你可能暈了。我們來看一下 put-some
函數(shù):
(defn put-some
[something]
(comp close-it #(put-in % something) open-it)) ;;返回值是一個函數(shù)葡粒!
這就是一個典型的閉包份殿。為什么呢膜钓?
你看,(comp close-it #(put-in 參數(shù)1 參數(shù)2) open-it)
卿嘲,本來是需要兩個參數(shù)的颂斜,然而在 put-some
中,也就是在 (comp close-it #(put-in 參數(shù)1 參數(shù)2) open-it)
的外層拾枣,對其 參數(shù)2 進(jìn)行了賦值沃疮,然后將這個函數(shù)作為返回值返回。
于是參數(shù) something
的值梅肤,就被包裹在 (comp ......something...)
中返回了出去司蔬。(有種偏函數(shù)的感覺)
其實 #(put-in % "elephant")
也可以看成是一個閉包 --- "elephant"
是由外界提供。
順帶一提姨蝴,Clojure 這個單詞就來自于閉包的英文 closure 配上 Java 的首字母 J 俊啼。
作者的絮叨:
終于找到工作了,成功的成為上班族似扔。
所以受限于本人的 Clojure 水平以及時間吨些,這個系列可能會慢下來了(喂,本來就更新的很慢好么)炒辉。
不過我會盡量繼續(xù)更新豪墅,繼續(xù)分享我的想法。
同樣繼續(xù)歡迎各位批評指正黔寇,畢竟我也是初學(xué)者偶器。只希望為 Clojure / Lisp 的普及做一點微小的貢獻(xiàn)(推眼鏡)。
下次見缝裤。