JavaScript函数柯里化的一些思考
1. 高阶函数的坑
在学习柯里化之前,我们首先来看下面一段代码:
- var f1 = function(x){
- return f(x);
- };
- f1(x);
很多同学都能看出来,这些写是非常傻的,因为函数f1
和f
是等效的,我们直接令var f1 = f;
就行了,完全没有必要包裹那么一层。
但是,下面一段代码就未必能够看得出问题来了:
- var getServerStuff = function(callback){
- return ajaxCall(function(json){
- return callback(json);
- });
- };
这是我摘自《JS函数式编程指南》中的一段代码,实际上,利用上面的规则,我们可以得出callback
与函数
- function(json){return callback(json);};
是等价的,所以函数可以化简为:
- var getServerStuff = function(callback){
- return ajaxCall(callback);
- };
继续化简:
- var getServerStuff = ajaxCall;
如此一来,我们发现那么长一段程序都白写了。
函数既可以当参数,又可以当返回值,是高阶函数的一个重要特性,但是稍不留神就容易踩到坑里。
2. 函数柯里化(curry)
言归正传,什么是函数柯里化?函数柯里化(curry)就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。听得很绕口,其实很简单,其实就是将函数的变量拆分开来调用:f(x,y,z) -> f(x)(y)(z)
。
对于最开始的例子,按照如下实现,要传入两个参数,f1
调用方式是f1(f,x)
。
- var f1 = function(f,x){
- return f(x);
- };
注意,由于f
是作为一个函数变量传入,所以f1
变成了一个新的函数。
我们将f1
变化一下,利用闭包可以写成如下形式,则f1
调用方式变成了f1(f)(x)
,而且得到的结果完全一样。这就完成了f1
的柯里化。
- var f1 = function(f){
- return function(x){
- return f(x);
- }
- };
- var f2 = f1(f);
- f2(x);
其实这个例子举得不恰当,细心的同学可能会发现,f1
虽然是一个新函数,但是f2
和f
是完全等效的,绕了半天,还是绕回来了。
这里有一个很经典的例子:
- ['11', '11', '11'].map(parseInt) //[ 11, NaN, 3 ]
- ['11', '11', '11'].map(f1(parseInt)) //[ 11, 11, 11 ]
由于parseInt
接受两个参数,所以直接调用会有进制转换的问题,参考“不愿相离”的文章。
var f2 = f1(parseInt)
,f2
让parseInt
由原来的接受两个参数变成了只接受一个参数的新函数,从而解决这个进制转换问题。通过我们的f1
包裹以后就能够运行出正确的结果了。
有同学觉得这个不算柯里化的应用,我觉得还是算吧,各位同学可以一起来讨论下。
3. 函数柯里化进一步思考
如果说上一节的例子中,我们不是直接运行f(x)
,而是把函数f
当做一个参数,结果会怎样呢?我们来看下面这个例子:
假设f1
返回函数g
,g
的作用域指向xs
,函数f
作为g
的参数。最终我们可以写成如下形式:
- var f1 = function(f,xs){
- return g.call(xs,f);
- };
实际上,用f1
来替代g.call(xxx)
的做法叫反柯里化。例如:
- var forEach = function(xs,f){
- return Array.prototype.forEach.call(xs,f);
- };
- var f = function(x){console.log(x);};
- var xs = {0:'peng',1:'chen',length:2};
- forEach(xs,f);
反curring就是把原来已经固定的参数或者this上下文等当作参数延迟到未来传递。
它能够在很大程度上简化函数,前提是你得习惯它。
抛开反柯里化,如果我们要柯里化f1
怎么办?
使用闭包,我们可以写成如下形式:
- var f1 = function(f){
- return function(xs){
- return g.call(xs,f);
- }
- };
- var f2 = f1(f);
- f2(xs);
把f
传入f1
中,我们就可以得到f2
这个新函数。
只传给函数一部分参数通常也叫做局部调用(partial application),能够大量减少样板文件代码(boilerplate code)。
当然,函数f1
传入的两个参数不一定非得包含函数+非函数,可能两个都是函数,也可能两个都是非函数。
我个人觉得柯里化并非是必须的,而且不熟悉的同学阅读起来可能会遇到麻烦,但是它能帮助我们理解JS中的函数式编程,更重要的是,我们以后在阅读类似的代码时,不会感到陌生。知乎上罗宸同学讲的挺好:
并非“柯里化”对函数式编程有意义。而是,函数式编程在把函数当作一等公民的同时,就不可避免的会产生“柯里化”这种用法。所以它并不是因为“有什么意义”才出现的。当然既然存在了,我们自然可以探讨一下怎么利用这种现象。
练习:
- // 通过局部调用(partial apply)移除所有参数
- var filterQs = function(xs) {
- return filter(function(x){ return match(/q/i, x); }, xs);
- };
- //这两个函数原题没有,是我自己加的
- var filter = function(f,xs){
- return xs.filter(f);
- };
- var match = function(what,x){
- return x.match(what);
- };
分析:函数filterQs
的作用是:传入一个字符串数组,过滤出包含’q'的字符串,并组成一个新的数组返回。
我们可以通过如下步骤得到函数filterQs
:
a. filter
传入的两个参数,第一个是回调函数,第二个是数组,filter
主要功能是根据回调函数过滤数组。我们首先将filter
函数柯里化:
- var filter = function(f){
- return function (xs) {
- return xs.filter(f);
- }
- };
b. 其次,filter
函数传入的回调函数是match
,match
的主要功能是判断每个字符串是否匹配what
这个正则表达式。这里我们将match
也柯里化:
- var match = function(what){
- return function(x){
- return x.match(what);
- }
- };
- var match2 = match(/q/i);
创建匹配函数match2
,检查字符串中是否包含字母q。
c. 把match2
传入filter
中,组合在一起,就形成了一个新的函数:
- var filterQs = filter(match2);
- var xs = ['q','test1','test2'];
- filterQs(xs);
从这个示例中我们也可以体会到函数柯里化的强大。所以,柯里化还有一个重要的功能:封装不同功能的函数,利用已有的函数组成新的函数。
4. 函数柯里化的递归调用
函数柯里化还有一种有趣的形式,就是函数可以在闭包中调用自己,类似于函数递归调用。如下所示:
- function add( seed ) {
- function retVal( later ) {
- return add( seed + later );
- }
- retVal.toString = function() {
- return seed;
- };
- return retVal;
- }
- console.log(add(1)(2)(3).toString()); // 6
add
函数返回闭包retVal
,在retVal
中又继续调用add
,最终我们可以写成add(1)(2)(3)(...)
这样柯里化的形式。
关于这段代码的解答,知乎上的李宏训同学回答地很好:
每调用一次add函数,都会返回retValue函数;调用retValue函数会调用add函数,然后还是返回retValue函数,所以调用add的结果一定是返回一个retValue函数。add函数的存在意义只是为了提供闭包,这个类似的递归调用每次调用add都会生成一个新的闭包。
5. 函数组合(compose)
函数组合是在柯里化基础上完成的:
- var compose = function(f,g) {
- return function(x) {
- return f(g(x));
- };
- };
- var f1 = compose(f,g);
- f1(x);
将传入的函数变成两个,通过组合的方式返回一个新的函数,让代码从右向左运行,而不是从内向外运行。
函数组合和柯里化有一个好处就是pointfree。
pointfree 模式指的是,永远不必说出你的数据。它的意思是说,函数无须提及将要操作的数据是什么样的。一等公民的函数、柯里化(curry)以及组合协作起来非常有助于实现这种模式。
- // 非 pointfree,因为提到了数据:name
- var initials = function (name) {
- return name.split(' ').map(compose(toUpperCase, head)).join('. ');
- };
- // pointfree
- var initials = compose(join('. '), map(compose(toUpperCase, head)), split(' '));
- initials("hunter stockton thompson");
- // 'H. S. T'
JavaScript函数柯里化的一些思考的更多相关文章
- JavaScript函数柯里化
函数式 JavaScript是以函数为一等公民的函数式语言.函数在JavaScript中也是一个对象(继承制Function),函数也可以作为参数传递成函数变量.最近几年函数式也因为其无副作用的特性. ...
- Javascript函数柯里化(curry)
函数柯里化currying,是函数式编程非常重要的一个标志.它的实现需要满足以下条件,首先就是函数可以作为参数进行传递,然后就是函数可以作为返回值return出去.我们依靠这个特性编写很多优雅酷炫的代 ...
- javascript函数柯里化初探
// 柯里化之前 function add(x,y,z){ return x+y+z; } add(1,2,3) // 6 // 柯里化之后 function curryAdd(x){ return ...
- 深入理解javascript函数进阶系列第二篇——函数柯里化
前面的话 函数柯里化currying的概念最早由俄国数学家Moses Schönfinkel发明,而后由著名的数理逻辑学家Haskell Curry将其丰富和发展,currying由此得名.本文将详细 ...
- 简单粗暴详细讲解javascript实现函数柯里化与反柯里化
函数柯里化(黑人问号脸)???Currying(黑人问号脸)???妥妥的中式翻译既视感:下面来一起看看究竟什么是函数柯里化: 维基百科的解释是:把接收多个参数的函数变换成接收一个单一参数(最初函数的第 ...
- JavaScript中的事件循环机制跟函数柯里化
一.事件循环机制的理解 test();//按秒输出5个5 function test() { for (var i = 0; i < 5; i++) { setTimeout(() => ...
- 一道javascript面试题(闭包与函数柯里化)
要求写一个函数add(),分别实现能如下效果: (1)console.log(add(1)(2)(3)(4)()); (2)console.log(add(1,2)(3,4)()); (3)conso ...
- 精读JavaScript模式(六),Memoization模式与函数柯里化的应用
假期就这么结束了!十天假就有三天在路上,真的难受!想想假期除了看了两场电影貌似也没做什么深刻印象的事情.流浪地球,特效还是很赞,不过对于感情的描写还是逃不掉拖沓和尴尬的通病,对于国产科幻还是抱有支持的 ...
- JavaScript之函数柯里化
什么是柯里化(currying)? 维基百科中的解释是:柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术.意思就是当函 ...
随机推荐
- 关于Java中的数组转变成字符串问题
1.用StringBuilder private static String arraytoString(int arr[]){ StringBuilder sb=new StringBuilder( ...
- jQuery 源码细读 -- $.Callbacks
$.Callbacks 是 jQuery 提供的可以方便地处理各种回调(callback)列表的类,其源代码是闭包的经典实现. 基本原理就是通过在闭包环境内保存一个 list = [] 数组用于存储回 ...
- MyEclipse使用经验总结
0.快捷键 ================================================================================ 编辑: Ctrl+Shif ...
- vs2015安装没有wim32
刚开始在官网上下载VS2015没在意太多,选择了默认安装,结果是没有win64的,所以就不能写c代码.默认安装很多库都没有,所以要什么都得下载.转载一篇文章
- Junit4 架构设计系列(2): Runner.run()与Statement
Overall 系列入口: Junit4 架构设计系列(1): Request,ClassRequest 和 RunnerBuilder 前文中,我们基本理清了Junit4执行Case大体上的Flow ...
- wget命令解析
今天一学信息安全的同学让我编写一个软件,功能大致如下:输入网站首页,自动下载该网站所有网页并保存?拿到后感觉属于搜索引擎相关的,说实话我就感觉会用到递归,不过我不会写,百度也没找到资料, ...
- 简单易学的机器学习算法——EM算法
简单易学的机器学习算法——EM算法 一.机器学习中的参数估计问题 在前面的博文中,如“简单易学的机器学习算法——Logistic回归”中,采用了极大似然函数对其模型中的参数进行估计,简单来讲即对于一系 ...
- Codeforces 161D
树形DP: 要求找出树上距离为k的点的对数: 对于每个节点,经过这个节点的符合条件的的点有两种: 第一种:距离他为i的儿子和他爸爸中距离他爸爸为k-i-1:(不是符合的点对中的一个) 第二种:他儿子中 ...
- A simple brute force problem.
hdu4971:http://acm.hdu.edu.cn/showproblem.php?pid=4971 题意:给你n个项目,每完成一个项目会有一定的收益,但是为了完成某个项目,要先学会一些技能, ...
- 居然还有WM_TIMECHANGE(只在用户手动改变系统时间时才会产生作用)
unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms ...