You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// 例 2varstr='function program'functionstringToUpper(str){returnstr.toUpperCase()}functionstringReverse(str){returnstr.split('').reverse().join('')}// var toUpperAndReverse = 组合(stringReverse, stringToUpper)// var res = toUpperAndReverse(str)functionstringToArray(str){returnstr.split('')}vartoUpperAndArray=组合(stringToArray,stringToUpper)toUpperAndArray(str)
JavaScript函数式编程,真香之认识函数式编程(一)
组合函数
组合是一种为软件的行为,进行清晰建模的一种简单、优雅而富于表现力的方式。通过组合小的、确定性的函数,来创建更大的软件组件和功能的过程,会生成更容易组织、理解、调试、扩展、测试和维护的软件。
对于组合,我觉得是函数式编程里面最精髓的地方之一,所以我迫不及待的把这个概念拿出来先介绍,因为在整个学习函数式编程里,所遇到的基本上都是以组合的方式来编写代码,这也是改变你从一个面向对象,或者结构化编程思想的一个关键点。
我这里也不去证明组合比继承好,也不说组合的方式写代码有多好,我希望你看了这篇文章能知道以组合的方式去抽象代码,这会扩展你的视野,在你想重构你的代码,或者想写出更易于维护的代码的时候,提供一种思路。
组合的概念是非常直观的,并不是函数式编程独有的,在我们生活中或者前端开发中处处可见。
比如我们现在流行的 SPA (单页面应用),都会有组件的概念,为什么要有组件的概念呢,因为它的目的就是想让你把一些通用的功能或者元素组合抽象成可重用的组件,就算不通用,你在构建一个复杂页面的时候也可以拆分成一个个具有简单功能的组件,然后再组合成你满足各种需求的页面。
其实我们函数式编程里面的组合也是类似,函数组合就是一种将已被分解的简单任务组织成复杂的整体过程。
现在我们有这样一个需求:给你一个字符串,将这个字符串转化成大写,然后逆序。
你可能会这么写。
可能看到这里你并没有觉得有什么不对的,但是现在产品又突发奇想,改了下需求,把字符串大写之后,把每个字符拆开之后组装成一个数组,比如 ’aaa‘ 最终会变成 [A, A, A]。
那么这个时候我们就需要更改我们之前我们封装的函数。这就修改了以前封装的代码,其实在设计模式里面就是破坏了开闭原则。
那么我们如果把最开始的需求代码写成这个样子,以函数式编程的方式来写。
那么当我们需求变化的时候,我们根本不需要修改之前封装过的东西。
可以看到当变更需求的时候,我们没有打破以前封装的代码,只是新增了函数功能,然后把函数进行重新组合。
突然产品又灵光一闪,又想改一下需求,把字符串大写之后,再翻转,再转成数组。
要是你按照以前的思考,没有进行抽象,你肯定心理一万只草泥马在奔腾,但是如果你抽象了,你完全可以不慌。
发现并没有更换你之前封装的代码,只是更换了函数的组合方式。可以看到,组合的方式是真的就是抽象单一功能的函数,然后再组成复杂功能。这种方式既锻炼了你的抽象能力,也给维护带来巨大的方便。
但是上面的组合我只是用汉字来代替的,我们应该如何去实现这个组合呢。首先我们可以知道,这是一个函数,同时参数也是函数,返回值也是函数。
我们看到例 2, 怎么将两个函数进行组合呢,根据上面说的,参数和返回值都是函数,那么我们可以确定函数的基本结构如下(顺便把组合换成英文的 compose)。
我们再思考一下,如果我们不用 compose 这个函数,在例 2 中怎么将两个函数合成呢,我们是不是也可以这么做来达到组合的目的。
那么按照这个逻辑是不是我们就可以写出
twoFuntonCompose
的实现了,就是同理我们也可以写出三个函数的组合函数,四个函数的组合函数,无非就是一直嵌套多层嘛,变成:
这种恶心的方式很显然不是我们程序员应该做的,然后我们也可以看到一些规律,无非就是把前一个函数的返回值作为后一个返回值的参数,当直接到最后一个函数的时候,就返回。
所以按照正常的思维就会这么写。
这样写没问题,underscore 也是这么写的,不过里面还有很多健壮性的处理,核心大概就是这样。
但是作为一个函数式爱好者,尽量还是以函数式的方式去思考,所以就用 reduceRight 写出如下代码。
当然对于 compose 的实现还有很多种方式,在这篇实现 compose 的五种思路中还给出了另外脑洞大开的实现方式,在我看这篇文章之前,另外三种我是没想到的,不过感觉也不是太有用,但是可以扩展我们的思路,有兴趣的同学可以看一看。
对于 compose 从最后一个函数开始求值的方式如果你不是很适应的话,你可以通过 pipe 函数来从左到右的方式。
实现跟 compose 差不多,只是把参数的遍历方式从右到左(reduceRight)改为从左到右(reduce)。
之前是不是看过很多文章写过如何实现 compose,或者柯里化,部分应用等函数,但是你可能不知道是用来干啥的,也没用过,所以记了又忘,忘了又记,看了这篇文章之后我希望这些你都可以轻松实现。后面会继续讲到柯里化和部分应用的实现。
point-free
在函数式编程的世界中,有这样一种很流行的编程风格。这种风格被称为 tacit programming,也被称作为 point-free,point 表示的就是形参,意思大概就是没有形参的编程风格。
有参的函数的目的是得到一个数据,而 pointfree 的函数的目的是得到另一个函数。
那这 pointfree 有什么用? 它可以让我们把注意力集中在函数上,参数命名的麻烦肯定是省了,代码也更简洁优雅。 需要注意的是,一个 pointfree 的函数可能是由众多非 pointfree 的函数组成的,也就是说底层的基础函数大都是有参的,pointfree 体现在用基础函数组合而成的高级函数上,这些高级函数往往可以作为我们的业务函数,通过组合不同的基础函数构成我们的复制的业务逻辑。
可以说 pointfree 使我们的编程看起来更美,更具有声明式,这种风格算是函数式编程里面的一种追求,一种标准,我们可以尽量的写成 pointfree,但是不要过度的使用,任何模式的过度使用都是不对的。
另外可以看到通过 compose 组合而成的基础函数都是只有一个参数的,但是往往我们的基础函数参数很可能不止一个,这个时候就会用到一个神奇的函数(柯里化函数)。
柯里化
在维基百科里面是这么定义柯里化的:
在定义中获取两个比较重要的信息:
这两个要点不是 compose 函数参数的要求么,而且可以将多个参数的函数转换成接受单一参数的函数,岂不是可以解决我们再上面提到的基础函数如果是多个参数不能用的问题,所以这就很清楚了柯里化函数的作用了。
柯里化函数可以使我们更好的去追求 pointfree,让我们代码写得更优美!
接下来我们具体看一个例子来理解柯里化吧:
比如你有一间士多店并且你想给你优惠的顾客给个 10% 的折扣(即打九折):
当一位优惠的顾客买了一间价值$500的物品,你给他打折:
你可以预见,从长远来看,我们会发现自己每天都在计算 10% 的折扣:
我们可以将 discount 函数柯里化,这样我们就不用总是每次增加这 0.10 的折扣。
现在,我们可以只计算你的顾客买的物品都价格了:
同样地,有些优惠顾客比一些优惠顾客更重要-让我们称之为超级客户。并且我们想给这些超级客户提供 20% 的折扣。
可以使用我们的柯里化的discount函数:
我们通过这个柯里化的 discount 函数折扣调为 0.2(即20%),给我们的超级客户配置了一个新的函数。
返回的函数 twentyPercentDiscount 将用于计算我们的超级客户的折扣:
我相信通过上面的 **discountCurry **你已经对柯里化有点感觉了,这篇文章是谈的柯里化在函数式编程里面的应用,所以我们再来看看在函数式里面怎么应用。
现在我们有这么一个需求:给定的一个字符串,先翻转,然后转大写,找是否有
TAOWENG
,如果有那么就输出 yes,否则就输出 no。我们很容易就写出了这四个函数,前面两个是上面就已经写过的,然后 find 函数也很简单,现在我们想通过 compose 的方式来实现 pointfree,但是我们的 find 函数要接受两个参数,不符合 compose 参数的规定,这个时候我们像前面一个例子一样,把 find 函数柯里化一下,然后再进行组合:
看到这里是不是可以看到柯里化在达到 pointfree 是非常的有用,较少参数,一步一步的实现我们的组合。
但是通过上面那种方式柯里化需要去修改以前封装好的函数,这也是破坏了开闭原则,而且对于一些基础函数去把源码修改了,其他地方用了可能就会有问题,所以我们应该写一个函数来手动柯里化。
根据定义之前对柯里化的定义,以及前面两个柯里化函数,我们可以写一个二元(参数个数为 2)的通用柯里化函数:
所以上面的 findCurry 就可以通过 twoCurry 来得到:
这样我们就可以不更改封装好的函数,也可以使用柯里化,然后进行函数组合。不过我们这里只实现了二元函数的柯里化,要是三元,四元是不是我们又要要写三元柯里化函数,四元柯里化函数呢,其实我们可以写一个通用的 n 元柯里化。
我这里采用的是递归的思路,当获取的参数个数大于或者等于 fn 的参数个数的时候,就证明参数已经获取完毕,所以直接执行 fn 了,如果没有获取完,就继续递归获取参数。
可以看到其实一个通用的柯里化函数核心思想是非常的简单,代码也非常简洁,而且还支持在一次调用的时候可以传多个参数(但是这种传递多个参数跟柯里化的定义不是很合,所以可以作为一种柯里化的变种)。
部分应用
部分应用是一种通过将函数的不可变参数子集,初始化为固定值来创建更小元数函数的操作。简单来说,如果存在一个具有五个参数的函数,给出三个参数后,就会得到一个、两个参数的函数。
看到上面的定义可能你会觉得这跟柯里化很相似,都是用来缩短函数参数的长度,所以如果理解了柯里化,理解部分应用是非常的简单:
debug
方法封装了我们平时用 console 对象调试的时候各种方法,本来是要传三个参数,我们通过部分应用的封装之后,我们只需要根据需要调用不同的方法,传必须的参数就可以了。我这个例子可能你会觉得没必要这么封装,根本没有减少什么工作量,但是如果我们在 debug 的时候不仅是要打印到控制台,还要把调试信息保存到数据库,或者做点其他的,那是不是这个封装就有用了。
因为部分应用也可以减少参数,所以他在我们进行编写组合函数的时候也占有一席之地,而且可以更快传递需要的参数,留下为了 compose 传递的参数,这里是跟柯里化比较,因为柯里化按照定义的话,一次函数调用只能传一个参数,如果有四五个参数就需要:
这种连续调用(这里所说的柯里化是按照定义的柯里化,而不是我们写的柯里化变种),但是用部分应用就可以:
既然我们现在已经明白了部分应用这个函数的作用了,那么还是来实现一个吧,真的是非常的简单:
另外不知道你有没有发现,这个部分应用跟 JavaScript 里面的 bind 函数很相似,都是把第一次穿进去的参数通过闭包存在函数里,等到再次调用的时候再把另外的参数传给函数,只是部分应用不用指定 this,所以也可以用 bind 来实现一个部分应用函数。
另外可以看到实际上柯里化和部分应用确实很相似,所以这两种技术很容易被混淆。它们主要的区别在于参数传递的内部机制与控制:
总结
在这篇文章里我重点想介绍的是函数以组合的方式来完成我们的需求,另外介绍了一种函数式编程风格:pointfree,让我们在函数式编程里面有了一个最佳实践,尽量写成 pointfree 形式(尽量,不是都要),然后介绍了通过柯里化或者部分应用来减少函数参数,符合 compose 或者 pipe 的参数要求。
所以这种文章的重点是理解我们如何去组合函数,如何去抽象复杂的函数为颗粒度更小,功能单一的函数。这将使我们的代码更容易维护,更具声明式的特点。
参考文章
文章首发于自己的个人网站桃园,另外也可以在 github blog 上找到。
如果有兴趣,也可以关注我的个人公众号:「前端桃园」
The text was updated successfully, but these errors were encountered: