Clojure 學(xué)習(xí)筆記 :11 函數(shù)組合

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)冰箱需要三步:

  1. 打開冰箱
  2. 把大象塞進(jìn)去
  3. 關(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)(推眼鏡)。

下次見缝裤。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末屏轰,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子憋飞,更是在濱河造成了極大的恐慌霎苗,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件榛做,死亡現(xiàn)場離奇詭異唁盏,居然都是意外死亡,警方通過查閱死者的電腦和手機检眯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,347評論 3 385
  • 文/潘曉璐 我一進(jìn)店門厘擂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人锰瘸,你說我怎么就攤上這事刽严。” “怎么了避凝?”我有些...
    開封第一講書人閱讀 157,435評論 0 348
  • 文/不壞的土叔 我叫張陵舞萄,是天一觀的道長眨补。 經(jīng)常有香客問我,道長倒脓,這世上最難降的妖魔是什么渤涌? 我笑而不...
    開封第一講書人閱讀 56,509評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮把还,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘茸俭。我一直安慰自己吊履,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,611評論 6 386
  • 文/花漫 我一把揭開白布调鬓。 她就那樣靜靜地躺著艇炎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪腾窝。 梳的紋絲不亂的頭發(fā)上缀踪,一...
    開封第一講書人閱讀 49,837評論 1 290
  • 那天,我揣著相機與錄音虹脯,去河邊找鬼驴娃。 笑死,一個胖子當(dāng)著我的面吹牛循集,可吹牛的內(nèi)容都是我干的唇敞。 我是一名探鬼主播,決...
    沈念sama閱讀 38,987評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼咒彤,長吁一口氣:“原來是場噩夢啊……” “哼疆柔!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起镶柱,我...
    開封第一講書人閱讀 37,730評論 0 267
  • 序言:老撾萬榮一對情侶失蹤旷档,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后歇拆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鞋屈,經(jīng)...
    沈念sama閱讀 44,194評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,525評論 2 327
  • 正文 我和宋清朗相戀三年查吊,在試婚紗的時候發(fā)現(xiàn)自己被綠了谐区。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,664評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡逻卖,死狀恐怖宋列,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情评也,我是刑警寧澤炼杖,帶...
    沈念sama閱讀 34,334評論 4 330
  • 正文 年R本政府宣布灭返,位于F島的核電站,受9級特大地震影響坤邪,放射性物質(zhì)發(fā)生泄漏熙含。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,944評論 3 313
  • 文/蒙蒙 一艇纺、第九天 我趴在偏房一處隱蔽的房頂上張望怎静。 院中可真熱鬧,春花似錦黔衡、人聲如沸蚓聘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,764評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽夜牡。三九已至,卻和暖如春侣签,著一層夾襖步出監(jiān)牢的瞬間塘装,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,997評論 1 266
  • 我被黑心中介騙來泰國打工影所, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蹦肴,地道東北人。 一個月前我還...
    沈念sama閱讀 46,389評論 2 360
  • 正文 我出身青樓型檀,卻偏偏與公主長得像冗尤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子胀溺,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,554評論 2 349

推薦閱讀更多精彩內(nèi)容