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 接受两个参数, 所以直接调用会有进制转换的问题, 参考 "不愿相离" https://www.cnblogs.com/liuyfl/p/4476179.html 的文章.
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 中的函数式编程, 更重要的是, 我们以后在阅读类似的代码时, 不会感到陌生. 知乎上罗宸 https://www.zhihu.com/question/20037482 同学讲的挺好:
并非 "柯里化" 对函数式编程有意义. 而是, 函数式编程在把函数当作一等公民的同时, 就不可避免的会产生 "柯里化" 这种用法. 所以它并不是因为 "有什么意义" 才出现的. 当然既然存在了, 我们自然可以探讨一下怎么利用这种现象.
练习:
- // 通过局部调用 (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. 函数柯里化的递归调用
函数柯里化还有一种有趣的形式 https://www.zhihu.com/question/20175380 , 就是函数可以在闭包中调用自己, 类似于函数递归调用. 如下所示:
- 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'
来源: http://www.bubuko.com/infodetail-3477550.html