函数柯里化的意义_RingCentral Tech丨函数不是越短越好的

▲点击“RingCentral铃盛软件”并设为【星标】,及时获取RingCentral的资讯

这篇文章的主要与大家分享一个来自于我日常 code review 的感想:虽然我们都喜欢自己编写的函数不要过长(这条经验也被囊括在了 Martin Fowler 的《重构》一书中), 但是太短的函数也不一定是更好的。当我们编写一个函数时, 函数的可读性、可维护性和鲁棒性要比其长短和抽象程度更重要。 R 一个简单的例子考虑以下场景: 你希望实现一个简单的算法来从一系列进账和消费中算出并以外汇的形式来展示一个银行账户的余额。为了简单起见, 接下来我将用  Javascript 来实现这一简单的需求, 并且 忽略ECMAScript中的  浮点问题 以及参数的  类型和值的合法性校验。这个算法显然需要3个参数: 账户余额、一个元素类型为Number的列表其中收入以正数表示且开销以负数表示、以及一个汇率, 返回值是一个Number类型的值。 R  简单粗暴的解法
function getBalanceInFC (    balance = 0, costs = [], exchangeRate = 1) {    return costs.reduce((acc, flow) => acc + flow, balance) / exchangeRate;}
R  利用可复合性

上述代码实际上是由两种数学运算构成: 累加和相除。两种运算在会计领域都有其对应的意义。

所以, 鉴于单一职责原则、可重用性和可维护性出发, 可以将其解构:

const getBalance = (balance = 0, costs = []) => costs.reduce((acc, flow) => acc + flow, balance);const getFC = (balance = 0, exchangeRate = 1) => balance / exchangeRate;const getBalanceInFC = (    balance = 0, costs = [], exchangeRate = 1) => getFC(getBalance(balance, costs), exchangeRate);
R  Point-free 风格以上的代码简单易懂且容易维护, 因为每个函数都只对应一个职责且均为纯函数。但抽象层及还能更高: 我们可以不写 getBalanceInFC的参数, 这种编程风格被称为 point-free 风格point-free 风格 来自于数学。就像几何更关注面和空间而胜过单个的点一样, 在编程中这种风格可以简单地视作是泛指那些不用提及其具体形参的函数。到达这种目的的途径之一是利用  eta-conversion[1]:

假设 z 单参数函数, 则:

x => z(x)

等同于:

z

自身.

但是我们还缺少一个材料, 因为 javascript 缺乏一个内建的combinator, 在Haskell 中我们称它为 after:

给定任意两个函数: f 和 g, 则表达式

x => f(g(x))

在 Haskell 中可以写作:

f.g

读作 f 在 g 之后 (f after g)

它也可以实现为前缀运算符:

after(f)(g)

注意这里的 after 是经过 柯里化 的.

我在这里提供一种 curry 和 after 的简单实现:

const curry = f => {    // Need to remove the default value for `f`    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/length#Description    const subCurry = (args) =>        args.length === f.length            ? f.apply(null, args)            : (x) => subCurry([...args, x]);    return x => subCurry([x]);}const after = f => g => x => f(g(x));

则为了能做 eta-conversion, 我们还需要柯里化后的 getBalance 和 getFC:

// the prefix `c` stands for `curry`const cGetBalance = curry(getBalance);const cGetFC = curry(getFC);

现在我们做好了朝 point-free 出发的准备, 我会用等式推导的的方式完成之后的内容:

1.编写柯里化的 getBalanceInFC:

    // the prefix `pf` stands for `point-free`    const pfGetBalanceInFC = (balance = 0) =>                                (costs = []) =>                                    (exchangeRate = 1) =>                                        getFC(getBalance(balance, costs), exchangeRate)

2.将 getFC 及其调用方式替换为它柯里化后的版本:

    const pfGetBalanceInFC = (balance = 0) =>                                (costs = []) =>                                    (exchangeRate = 1) =>                                        cGetFC(getBalance(balance, costs))(exchangeRate);

3.用 eta-conversion 消除实参exchangeRate:

    const pfGetBalanceInFC = (balance = 0) => (costs = []) => cGetFC(getBalance(balance, costs));

4.getBalance 及其调用方式替换为它柯里化后的版本:

    const pfGetBalanceInFC = (balance = 0) => (costs = []) => cGetFC(cGetBalance(balance)(costs));

5.接下来我们利用柯里化的优势 —— cGetBalance(balance) 是一个函数! 我们可以假设 cGetBalance(balance) 等效于某个函数 f, 则 cGetFC(cGetBalance(balance)(costs)) 可以读作 cGetFC  f 之后 然后用 costs 来调用 (cGetFC after f then applied with costs)。然后我们再利用函数式编程在 声明式编程 (declarative) 上的优势来将自然语言翻译成代码:

    const pfGetBalanceInFC        // = (balance = 0) => (costs = []) => after(cGetFC)(cGetBalance(balance))(costs);        = (balance = 0) => after(cGetFC)(cGetBalance(balance));
6. 对上一步得出的表达式, 我们可以应用一条规则:  f.g ≅ \x->(f.g)(x) ≅ \x->f(g(x))  (为了简单起见, 我在这里混用了  Haskell  语法和范畴论中的同构运算符≅)。之后让我们再一次应用这条规则: 把 after(cGetFC)  看作是某一个函数表达式 f 并且把 cGetBalance 看作是某个函数 g 后我们就能看清所得到的结果恰好匹配上述的规则:
    const pfGetBalanceInFC        // = (balance = 0) => after(after(cGetFC))(cGetBalance)(balance);        = after(after(cGetFC))(cGetBalance);

所以 point-free 风格的 getBalanceInFC

就是 after(after(cGetFC))(cGetBalance)

它很简短又很抽象但是让人完全摸不着头脑!即使point-free 风格有它自己的优势 —— 如果我们把上述的模式抽出一个 combinator 的话:

const owl    // = f => g => after(after(f))(g);    = after(after)(after); // applying same rules above

值得一提的是它的 Haskell 版本看起来更"可怕":

-- because it looks like an owlowl = (.).(.)

则这个 猫头鹰 (owl) 对任意参数数量的函数f都适用。给与任意像 getBalance 那样拥有2个形参的函数:

const f = (x, y, z) => x + y + z;const g = (x, y) => x / y;const p = (w, x, y, z) => f((g(w, x)), y, z);const owlP = owl(curry(f))(curry(g));console.log(p(1, 2, 3, 4) === owlP(1)(2)(3)(4)) //true

这意味着给定同样的参数, 则powlP的执行结果始终一致(可以利用我们的 after combinator 的类型注解来得以证明这一断言)。无可辩驳的是猫头鹰的确提供了鲁棒性, 但是也极大增加了理解代码的负担。

R  总结

编程实践是一种社交活动, 因为其中涉及到了合作和交流。而交流则需要参与者拥有一定的背景知识。举个例子, 每当人们向我询问 monad 的概念时, 最精炼的答案总是来自于 James Iry 的文章 Brief, Incomplete and Mostly Wrong History of Programming Languages[2] 中的一段话:

A monad is just a monoid in the category of endofunctors

我相信大多数人听到这个答案的时候, 对话就结束了。因为为了能够理解这个答案的意思, 听者需要理解一大堆数学概念: 像  前序集幺半群(monoid)范畴(category), 以及  自函子(endofunctors);在此之外还得再了解  Galois connectionadjoint situation函子(functor)natural transformations。但是如果我从一个简单的  commutative diagram 开始画起(我最喜欢范畴论的一点就在于可以用画图来证明东西), 我就能用点和线来让提问者理解范畴论的概念, 进而再解释  Kleisli Categories, 也许最后他们就能对这个复杂的概念有比较直观的认识。程序中抽象的优势在于对于特定的问题域上能够在提供特定的功能的同时又给与鲁棒性。但是程序员应该给予其合作者的知识背景来选择其代码的抽象程度, 尤其是在企业规模的实践中。所以我认为一个拥有好味道的的函数代码永远是那些对于团队成员来说易读、易懂又不失鲁棒性的函数, 而其长短则没那么重要。 R 参考 1. 关于   owl   的更多信息可以从这本逻辑猜谜书中找到  To Mock a Mockingbird[3]

     2.这里提供一个 Haskell 的版本:

    -- because it looks like an owl    owl = (.).(.)    getBalance:: Float -> [Float] -> Float    getBalance original costs = foldl (+) original costs    getFC:: Float -> Float -> Float    getFC balance exchangeRate = balance / exchangeRate    getBalanceInFC = getFC `owl` getBalance    main = print $ getBalanceInFC 1 [1, 2] 2

3.这里用近似 Haskell 的语法给出一个owl结合子的通用性的证明:

根据after的类型注解:

(.):: (b → C) → (a → b) → (a → c)

我们想得出一下结合子的类型注解:

 (.).(.)

∵ 让我们将上式子的左右2个 (.) 分别表上序号为 #1, #2; 并且将中间的.标注为 #3, 则:

 #1:: (b→c)→(a→b)→(a→c) #2:: (b1→c1)→(a1→b1)→(a1→c1) #3:: (b2→c2)→(a2→b2)→(a2→c2)

∴ 我们有:

 #1:: b2→c2 #2:: a2→b2

∴ 进而推导出:

 b2:: b→c c2:: (a→b)→(a→c)

以及:

 a2:: b1→c1 b2:: (a1→b1)→(a1→c1)

这意味着:

 b2:: b→c   :: (a1→b1)→(a1→c1)

∴ 得:

 b::c1→b1 c::a1→c1

因此最终结果是:

 (.).(.):: a2→c2        :: (b1 → c1)→[a→(a1→b1)]->[a→(a1→c1)]
这和你在 GHC 中输入  :type (.).(.) 得到的结果一模一样(除了类型的命名和方括号外)。因此一旦你“喂给”  owl 两个函数以及用于第二个函数的两个参数后, 你将得到类型为  c1 的结果, 它就是你给的第一个函数进行(部分)评估后的结果的类型注解。这也是为什么第一个函数可以获得任意数量的参数的原因。

访问原文请点击文末

511c9c990f98211833c9d7277e2b20eb.png

1f657283d77daf20966ad8d7f8445ae4.gif

点击阅读原文

即可查看原文

点个在看

少个Bug

0ce1ada5a3e1e731b01ec27ff7658dfd.gif
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 内容概要 《计算机试卷1》是一份综合性的计算机基础和应用测试卷,涵盖了计算机硬件、软件、操作系统、网络、多媒体技术等多个领域的知识点。试卷包括单选题和操作应用两大类,单选题部分测试学生对计算机基础知识的掌握,操作应用部分则评估学生对计算机应用软件的实际操作能力。 ### 适用人群 本试卷适用于: - 计算机专业或信息技术相关专业的学生,用于课程学习或考试复习。 - 准备计算机等级考试或职业资格认证的人士,作为实战演练材料。 - 对计算机操作有兴趣的自学者,用于提升个人计算机应用技能。 - 计算机基础教育工作者,作为教学资源或出题参考。 ### 使用场景及目标 1. **学习评估**:作为学校或教育机构对学生计算机基础知识和应用技能的评估工具。 2. **自学测试**:供个人自学者检验自己对计算机知识的掌握程度和操作熟练度。 3. **职业发展**:帮助职场人士通过实际操作练习,提升计算机应用能力,增强工作竞争力。 4. **教学资源**:教师可以用于课堂教学,作为教学内容的补充或学生的课后练习。 5. **竞赛准备**:适合准备计算机相关竞赛的学生,作为强化训练和技能检测的材料。 试卷的目标是通过系统性的题目设计,帮助学生全面复习和巩固计算机基础知识,同时通过实际操作题目,提高学生解决实际问题的能力。通过本试卷的学习与练习,学生将能够更加深入地理解计算机的工作原理,掌握常用软件的使用方法,为未来的学术或职业生涯打下坚实的基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值