在学习柯里化之前,我们首先来看下面一段代码:
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;
如此一来,我们发现那么长一段程序都白写了。
函数既可以当参数,又可以当返回值,是高阶函数的一个重要特性,但是稍不留神就容易踩到坑里。
言归正传,什么是函数柯里化?函数柯里化(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包裹以后就能够运行出正确的结果了。
如果说上一节的例子中,我们不是直接运行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);
从这个示例中我们也可以体会到函数柯里化的强大。所以,柯里化还有一个重要的功能:封装不同功能的函数,利用已有的函数组成新的函数。
函数柯里化还有一种有趣的形式,就是函数可以在闭包中调用自己,类似于函数递归调用。如下所示:
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都会生成一个新的闭包。
函数组合是在柯里化基础上完成的:
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'
理论计算机科学中,柯里化提供了在简单的理论模型中,比如:只接受一个单一参数的lambda演算中,研究带有多个参数的函数的方式。 函数柯里化的对偶是Uncurrying,一种使用匿名单参数函数来实现多参数函数的方法。
很多刚刚了解函数式编程的人会对偏函数应用(partial application)和柯里化(currying)之间的区别感到困惑。实际上,直到现在也很少在 JavaScript 中看到柯里化的实际使用,许多叫curry()的工具函数并不是柯里化函数。它们其实是偏函数!
柯里化,是一个逐步接收参数的过程。在接下来的剖析中,你会深刻体会到这一点。 反柯里化,是一个泛型化的过程。它使得被反柯里化的函数,可以接收更多参数。目的是创建一个更普适性的函数,可以被不同的对象使用。
如果我们需要设计一个函数来计算每个月的开销,在每天结束之前,我们需要记录当天花费了多少。在月底的时候计算出这个月一共开销了多少? 可以看出在性能上和方式二差不多,但是这样做更加通俗易懂了,当然在实际开发中,我们一般会封装为对象。
我们经常说在Javascript语言中,函数是“一等公民”,它们本质上是十分简单和过程化的。可以利用函数,进行一些简单的数据处理,return 结果,或者有一些额外的功能,需要通过使用闭包来实现,最后经常会return 匿名函数。
柯里化(Currying),又称部分求值(Partial Evaluation),是把接收多个参数的函数变成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受剩余的参数而且返回结果的新函数的技术。
把接收多个参数的函数变换成接收一个单一参数(最初函数的第一个参数)的函数,并返回接受剩余的参数而且返回结果的新函数的技术。其由数学家Haskell Brooks Curry提出,并以curry命名。
最近在社区阅读技术博客的时候偶然间看到了函数柯里化几个字,还有要求手写js函数柯里化,心想是柯里化是什么高级的东西?没听说过啊?就带着问题出发,专门去学习了一下,做了一些整理。
函数式编程是一种如今比较流行的编程范式,它主张将函数作为参数进行传递,然后返回一个没有副作用的函数,说白了,就是希望一个函数只做一件事情。这种编程思想涵盖了三个重要的概念:
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!