JS 函数式编程思维简述(三):柯里化

JavaScript045

JS 函数式编程思维简述(三):柯里化,第1张

       在探讨柯里化之前,我们首先聊一聊很容易跟其混淆的另一个概念—— 偏函数(Partial Application) 。在维基百科中,对 Partial Application 的定义是这样的:

其含义是:在计算机科学中,局部应用(或偏函数应用)是指将 多个参数 固定在一个函数中,从而 产生另一个函数 的过程。

举个例子,假设我们是一个加工厂,用于生产梯形的零件,生产过程中我们要根据 订单来源方 给的一系列参数计算面积:

突然有一天,我们发现了一个问题:我们的大部分订单零件,都是高度为 28 的规格,此时面积函数调用经常是这个样子的:

此时,我们便可以 以第一个函数为模板 ,来创建 存储了固定值的新的计算函数

当然,这个示例中并没有以明显的 偏函数 的方式去呈现,我们可以让返回结果变成一个新的函数,因此我们可以加以改造:

也可以将其简化为:

这里,我们就可以将 trapezoidAreaByHeight15() 、 trapezoidAreaByHeight28() 和 trapezoidAreaByHeight33() 视为 trapezoidArea() 的偏函数。

        偏函数 往往不能改变一个函数的行为,通常是根据一个已有函数而生成一个新的函数,这个新的函数具有已有函数的相同功能,区别在于在新的函数中有一些参数 已被固定 不会变更。偏函数的设计通常:

        柯里化(Currying) 是以美国数理逻辑学家哈斯凯尔·科里(Haskell Curry)的名字命名的函数应用方式。 与偏函数很像的地方是:都可以缓存参数,都会返回一个新的函数,以提高程序中函数的适用性。 而不同点在于, 柯里化(Currying) 通常用于分解原函数式,将参数数量为 n 的一个函数,分解为参数数量为 1 的 n 个函数,并且支持连续调用。例如:

可见, 柯里化(Currying) 用于将多元任务分解成单一任务,每一个独立的任务都 缓存了上一次函数生成时传递的入参 ,并且让新生成的函数更简单、专注。上述演变也可以写作:

        柯里化(Currying) 分解了函数设计过程,将运行的步骤拆分为每一个单一参数的 lambda 演算。这里例举一个在 JavaScript 中用于做强制类型判断的示例:

使用这一的方式构建的函数 checkType() 具备了高通用性,但适用性则略差。我们发现 每次的调用过程,使用者都需要编写参数 typeStr 表示的类型字符串 ,增加了函数的应用复杂度。此时作为设计者,就可以对该函数加以改造,使其生成多个具备高适用性的独立函数:

        柯里化(Currying) 分解了函数设计过程,将运行的步骤拆分为每一个单一参数的 lambda 演算。我们可以通过递归的方式,来构造出一个可进行无限调用,并返回相同的累加函数的 柯里化函数

调用方式如:

以这样的方式,我们构建的参数是一个简单对象 nexter ,该对象至少包含一个 value 属性,用于描述本次累加的值。如果希望获取累加结果,则为 nexter 对象赋予函数属性 success 即可。结果会以实参的形式,传递给 success 函数用于传递通知。

        Promise 对象无论是构造函数还是后续的链式调用中,都能看到 柯里化 设计的影子:接收单一参数,返回一个 Promise :

调用方式为:

第一次看到柯里化这个词的时候,还是在看一篇算法相关的博客提到把函数柯里化,那时一看这个词就感觉很高端,实际上当你了解了后才发现其实就是高阶函数的一个特殊用法。

果然是不管作用怎么样都要有个高端的名字才有用。

看这个解释有一点抽象,我们就拿被做了无数次示例的add函数,来做一个简单的实现。

实际上就是把add函数的x,y两个参数变成了先用一个函数接收x然后返回一个函数去处理y参数。现在思路应该就比较清晰了,就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

但是问题来了费这么大劲封装一层,到底有什么用处呢?没有好处想让我们程序员多干事情是不可能滴,这辈子都不可能.

上面的示例是一个正则的校验,正常来说直接调用check函数就可以了,但是如果我有很多地方都要校验是否有数字,其实就是需要将第一个参数reg进行复用,这样别的地方就能够直接调用hasNumber,hasLetter等函数,让参数能够复用,调用起来也更方便。

我们在做项目的过程中,封装一些dom操作可以说再常见不过,上面第一种写法也是比较常见,但是我们看看第二种写法,它相对一第一种写法就是自执行然后返回一个新的函数,这样其实就是提前确定了会走哪一个方法,避免每次都进行判断。

像我们js中经常使用的bind,实现的机制就是Currying.

说了这几点好处之后,发现还有个问题,难道每次使用Currying都要对底层函数去做修改,

这边首先是初步封装,通过闭包把初步参数给保存下来,然后通过获取剩下的arguments进行拼接,最后执行需要currying的函数。

但是好像还有些什么缺陷,这样返回的话其实只能多扩展一个参数,currying(a)(b)(c)这样的话,貌似就不支持了(不支持多参数调用),一般这种情况都会想到使用递归再进行封装一层。

这边其实是在初步的基础上,加上了递归的调用,只要参数个数小于最初的fn.length,就会继续执行递归。

curry的一些性能问题你只要知道下面四点就差不多了: