Clojure 学习笔记 :11 函数组合

Clojure 零基础 学习笔记 偏函数 串行宏 高阶函数 闭包


函数组合 --- 简单而又有力的武器

在函数式编程中,我们偏爱使用不可变值声明式的处理,以及函数组合来解决问题。我们已经在之前的章节里简要介绍了不可变值以及声明式遍历,而函数组合,其实早在一开始学会如何使用函数的时候,就已经开始运用这种技巧了。

提起可组合性,你可能会想到面向对象编程中把一个对象包裹在另一个对象中的行为,也许脑海中还会蹦出几个设计模式的名字,或者你对其一无所知脑中一片空白。不过这都不重要,暂时忘记之前你所了解到的繁琐的面向对象编程中的组合,这次我们所介绍的函数组合简单易用又强大。

何为函数组合?顾名思义,就是把多个函数拧巴在一起形成一个新函数。
Clojure 提供了许多工具来帮助你进行函数组合。


comp

使用 comp 函数(也就是英文 composition 的前四个字母)可以把几个函数组合成一个函数。
把大象装进冰箱需要三步:

  1. 打开冰箱
  2. 把大象塞进去
  3. 关上冰箱
(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)
;; 上述表达式的值为
;= {:open? false, :content [milk apple elephant]}

如果前一个执行的函数的返回值并不能作为后一个函数的参数,那么在执行的时候就会出现问题。
注意,comp 组合的函数执行顺序是从右往左的。
如果不使用 comp,那么也可以有下面这样等价的调用方式:

(close-it (put-elephant-in (open-it refrigerator)))

这也是为啥 comp 要以看起来很奇怪的从右往左的顺序执行的原因。

通常情况下,简单起见,类似 put-elephant-in 这种用于某一特定情形下只使用一次的函数,可以使用“匿名函数的字面量”来简化它。(函数字面量在第 8 节 Clojure 学习笔记 :8 遍历元素 中有所介绍。)
也就是不需要单独定义它,而是直接在需要的位置填写函数字面量:

((comp close-it #(put-in % "elephant") open-it) refrigerator)
;= {:open? false, :content [milk apple elephant]}

为了使之应用于更广泛的行为 --- 把任意东西放进冰箱,可以再次将其改写为一个高阶函数:

(defn put-some
  [something]
  (comp close-it #(put-in % something) open-it)) ;;返回值是一个函数!

然后就可以这么来使用了:

((put-some "elephant") refrigerator)
;= {:open? false, :content [milk apple elephant]}

第五集中,我们已经简单接触了高阶函数。所谓高阶函数,就是说这个函数可以接受函数作为参数,或,返回值是函数。按照这个说法,高阶函数其实随处可见。比如上面介绍的 comp 自然就属于高阶函数。
除了使用 Clojure 预先提供的高阶函数来进行函数组合,我们还可以自己来编写高阶函数,比如 put-some 函数。


串行宏

->->> 称为串行宏,它的功能与 comp 基本一致,如果你不想用 comp 那么可以试试这两款。

(-> (open-it refrigerator)
    (put-elephant-in)
    (close-it))
;= {:open? false, :content [milk apple elephant]}

;; 当然也可以使用函数字面量
(-> (open-it refrigerator)
    #(put-in % "elephant")
    (close-it))
;= {:open? false, :content [milk apple elephant]}

-> (一个减号,一个大于号),它接受一系列表达式,并把第一个表达式的值作为第二个表达式的第一个参数,然后求出第二个表达式的值,然后再将这个值作为第三个表达式的第一个参数……
这样说可能并不是很清晰。我们用更形象的方式来描述一下。

(-> (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))))
;; 无法再继续移动,移动完毕,最终结果
;; (close-it (put-elephant-in (open-it refrigerator)))
;= {:open? false, :content [milk apple elephant]}

->> (一个减号,两个大于号),它的效果与 -> 显著的区别就是:
-> 把上一表达式的值作为下一表达式的第一个参数
->> 是把上一表达式的值作为下一表达式的最后一个参数
你可以这么来记忆,“长箭头会把内容向右推的更远,而短箭头力气比较小,所以只能推到第一个参数的位置”。

另外,如果后续表达式所需的参数只有一个(如本例),也就是无需区分第一个参数与最后一个参数,那么使用两种串行宏的效果都是一样的。
而且,后续表达式无需使用括号括起来,也一样可以执行:

(-> (open-it refrigerator)
    put-elephant-in
    close-it)
;= {:open? false, :content [milk apple elephant]}

小贴士:
这里所说的 宏 (macro) 并不是函数。与函数不同,宏会在代码“运行”之前对代码做一些“调整”,它是 Lisp 的终极武器。


偏函数

在进行函数组合的时候,你可能需要固定某个函数的前几个参数值,比如设置一些默认值,简化使用。在 Clojure 中可以使用 partial 来实现这种功能,称为偏函数。
举个栗子,比如假设有一个函数用来访问服务器:

(connect-server "8.8.8.8" "53" "data.........")

每次都要输入服务器 IP 和 端口是不是太麻烦了?这时候 partial 就派上用场了。

(def connect-googledns (partial connect-server "8.8.8.8" "53"))
(connect-googledns "data1.........")
(connect-googledns "data2.........")

partial 是一个高阶函数,它效果是构造出一个新函数并返回这个新函数,新函数其实是预先指定了老函数开头的几个参数。
你可能会发现其实函数字面量或者自己手写高阶函数也可以实现类似的功能:

(def connect-googledns #(connect-server "8.8.8.8" "53" %))
;; 或者
(defn connect-googledns
  [data]
  (connect-server "8.8.8.8" "53" data))

而且它们还不限制指定参数的顺序,partial 却必须以顺序指定参数。的确是这样。
但是 partial 的优点是不需要了解函数有多少个参数,只指定第一个参数一样可以工作:

(partial connect-server "8.8.8.8")

字面量则需要手动填上每个参数的位置:

#(connect-server "8.8.8.8" %1 %2)

所以,如果函数的参数个数可变或者个数比较多,你又想固定开头的某些参数,那么你可以考虑 partial


闭包

这个概念看起来很神秘。其实在上面的例子中,我们已经使用了闭包。
闭包的表象是:一个高阶函数 A,它返回一个函数,而且返回的函数的某些参数由 A 来提供。
或者说:某个函数的参数由外部作用域提供,而不是自身作用域提供。
也就是说,闭包的不严谨定义就是:某一局部绑定的值在其生存期外依然可以被访问,因为这个局部绑定被某种东西“包裹”了起来(在 Clojure 中也就是作为函数参数,被函数包裹起来),然后被作为返回值返回了。这个返回值被其它位置引用,所以依然不会被回收。

好吧你可能晕了。我们来看一下 put-some 函数:

(defn put-some
  [something]
  (comp close-it #(put-in % something) open-it)) ;;返回值是一个函数!

这就是一个典型的闭包。为什么呢?
你看,(comp close-it #(put-in 参数1 参数2) open-it) ,本来是需要两个参数的,然而在 put-some 中,也就是在 (comp close-it #(put-in 参数1 参数2) open-it) 的外层,对其 参数2 进行了赋值,然后将这个函数作为返回值返回。
于是参数 something 的值,就被包裹在 (comp ......something...) 中返回了出去。(有种偏函数的感觉)

其实 #(put-in % "elephant") 也可以看成是一个闭包 --- "elephant" 是由外界提供。

顺带一提,Clojure 这个单词就来自于闭包的英文 closure 配上 Java 的首字母 J 。


作者的絮叨:
终于找到工作了,成功的成为上班族。
所以受限于本人的 Clojure 水平以及时间,这个系列可能会慢下来了(喂,本来就更新的很慢好么)。
不过我会尽量继续更新,继续分享我的想法。
同样继续欢迎各位批评指正,毕竟我也是初学者。只希望为 Clojure / Lisp 的普及做一点微小的贡献(推眼镜)。

下次见。


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容