函数柯里化学习笔记

导读:函数柯里化currying的概念最早由俄国数学家Moses Schönfinkel发明,而后由著名的数理逻辑学家Haskell Curry将其丰富和发展,currying由此得名。
定义:currying又称部分求值。一个currying的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
一个简单curry的栗子

function add(a, b) {
    return a + b;
}

//函数只能传一个参数时候实现加法
function curry(a) {
    return function(b) {
        return a + b;
    }
}
var add2 = curry(2); //add2也就是第一个参数为2的add版本
console.log(add2(3))//5

通过以上简单介绍我们大概了解了,函数柯里化基本是在做这么一件事情:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。用公式表示就是我们要做的事情其实是

fn(a,b,c,d)=>fn(a)(b)(c)(d);

fn(a,b,c,d)=>fn(a,b)(c)(d);

fn(a,b,c,d)=>fn(a)(b,c,d);

......

再或者这样:

fn(a,b,c,d)=>fn(a)(b)(c)(d)();

fn(a,b,c,d)=>fn(a);fn(b);fn(c);fn(d);fn();

但不是这样:

fn(a,b,c,d)=>fn(a);

fn(a,b,c,d)=>fn(a,b);

......

这类不属于柯里化内容,它也有个专业的名字叫偏函数,这个之后我们也会提到。
下面我们继续把之前的add改为通用版本:

const curry = (fn, ...arg) => {
    let all = arg;
    return (...rest) => {
        all.push(...rest);
        return fn.apply(null, all);
    }
}
let add2 = curry(add, 2)
console.log(add2(8));    //10
add2 = curry(add);
console.log(add2(2,8)); //10

如果你想给函数执行绑定执行环境也很简单,可以多传入个参数:

const curry = (fn, constext, ...arg) => {
    let all = arg;
    return (...rest) => {
        all.push(...rest);
        return fn.apply(constext, all);
    }
}

不过到目前我们并没有实现柯里化,就是类似fn(a,b,c,d)=>fn(a)(b)(c)(d),这样的转化,原因也很明显,我们curry之后的add2函数只能执行一次,不能够sdd2(5)(8)这样执行,因为我们没有在函数第一次执行完后返回一个函数,而是返回的值,所以无法继续调用。

所以我们继续实现我们的curry函数,要实现的点也明确了,柯里化后的函数在传入参数未达到柯里化前的个数时候我们不能返回值,应该返回函数让它继续执行(如果你阅读到这里可以试着自己实现一下),下面给出一种简单的实现方式:

const curry = (fn, ...arg) => {
    let all = arg || [],
        length = fn.length;
    return (...rest) => {
        let _args = all.slice(0); //拷贝新的all,避免改动公有的all属性,导致多次调用_args.length出错
        _args.push(...rest);
        if (_args.length < length) {
            return curry.call(this, fn, ..._args);
        } else {
            return fn.apply(this, _args);
        }
    }
}
let add2 = curry(add, 2)
console.log(add2(8));//10
console.log(add2(8, 1));//10
console.log(add2(8)(1));//error
add2 = curry(add);
console.log(add2(2, 8));
console.log(add2(2)(8));
let test = curry(function(a, b, c) {
console.log(a + b + c);
})
test(1, 2, 3);
test(1, 2)(3);
test(1)(2)(3);

这里代码逻辑其实很简单,就是判断参数是否已经达到预期的值(函数柯里化之前的参数个数),如果没有继续返回函数,达到了就执行函数然后返回值,唯一需要注意的点我在注释里写出来了all相当于闭包引用的变量是公用的,需要在每个返回的函数里拷贝一份;

好了到这里我们基本实现了柯里化函数,我们来看文章开始罗列的公式,细心的同学应该能发现:

fn(a,b,c,d)=>fn(a)(b)(c)(d)();//mod1

fn(a,b,c,d)=>fn(a);fn(b);fn(c);fn(d);fn();//mod2

这两种我们的curry还未实现,对于这两个公式其实是一样的,写法不同而已,对比之前的实现就是多了一个要素,函数执行返回值的触发时机和被柯里化函数的参数的不确定性,好了我们来简单修改一下代码:

const curry = (fn, ...arg) => {
    let all = arg || [],
        length = fn.length;
    return (...rest) => {
        let _args = all;
        _args.push(...rest);
        if (rest.length === 0) {
       all=[];
            return fn.apply(this, _args);
        } else {
            return curry.call(this, fn, ..._args);
        }
    }
}
let test = curry(function(...rest) {
    let args = rest.map(val => val * 10);
    console.log(args);
})
test(2);
test(2);
test(3);
test();//[20, 20, 30]
test(5);
test();//[50]
test(2)(2)(2)(3)(4)(5)(6)();//
test(2, 3, 4, 5, 6, 7)();//

现在我们这个test函数的参数就可以任意传,可多可少,至于在什么时候执行返回值,控制权在我们(这里是设置的传入参数为空时候触发函数执行返回值),当然根据这逻辑我们能改造出来很多我们期望它按我们需求传参、执行的函数——这里我们就体会到了高阶函数的灵活多变,让使用者有更多发挥空间。

到这里我们科里化基本说完了,下面我们顺带说一下偏函数,如果你上边柯里化的代码都熟悉了,那么对于偏函数的这种转化形式应该得心应手了:

fn(a,b,c,d)=>fn(a);

fn(a,b,c,d)=>fn(a,b);

我们还是先来看代码吧

function part(fn, ...arg) {
    let all = arg || [];
    return (...rest) => {
        let args = all.slice(0);
        args.push(...rest);
        return fn.apply(this, args)
    }
}

function add(a = 0, b = 0, c = 0) {
    console.log(a + b + c);
}
let addPart = part(add);
addPart(9); //9
addPart(9, 11);//20

很简单了,我们现在的addPar就能随便传参都能调用了,当然我们也能控制函数之调用某一个或者多个参数,例如这样:

        //偏han shu 
        function part(fn) {
            return (...arguments) => {
                return fn.call(this, arguments[0])
            }
        };      
        let newA = ['33','222','999','99888','2345'].map(part(parseInt));
        console.log('newA is ', newA)

我们想用parseInt帮我们转化个数组,但是我们没法改动parseInt的代码,所以控制一下传参就行了,这样我们map就传入的参数只取到第一个,得到了我们的期望值。

Function.prototype.bind 方法也是柯里化应用

与 call/apply 方法直接执行不同,bind 方法 将第一个参数设置为函数执行的上下文,其他参数依次传递给调用方法(函数的主体本身不执行,可以看成是延迟执行),并动态创建返回一个新的函数, 这符合柯里化特点。

var foo = {x: 888};
var bar = function () {
    console.log(this.x);
}.bind(foo);               // 绑定
bar();                     // 888

下面是一个 bind 函数的模拟,testBind 创建并返回新的函数,在新的函数中将真正要执行业务的函数绑定到实参传入的上下文,延迟执行了。

Function.prototype.testBind = function (scope) {//提前固定易变参数。
    var fn = this;                    // this 指向的是调用 testBind 方法的一个函数, 
    return function () {
        return fn.apply(scope);
    }
};
var testBindBar = bar.testBind(foo);  // 绑定 foo,延迟执行
console.log(testBindBar);             // Function (可见,bind之后返回的是一个延迟执行的新函数)
testBindBar(); 

这里要注意 prototype 中 this 的理解。

反柯里化

Array.prototype上的方法原本只能用来操作array对象。但用call和apply可以把任意对象当作this传入某个方法,这样一来,方法中用到this的地方就不再局限于原来规定的对象,而是加以泛化并得到更广的适用性

有没有办法把泛化this的过程提取出来呢?反柯里化(uncurrying)就是用来解决这个问题的。反柯里化主要用于扩大适用范围,创建一个应用范围更广的函数。使本来只有特定对象才适用的方法,扩展到更多的对象。

uncurrying的话题来自JavaScript之父Brendan Eich在2011年发表的一篇文章。以下代码是 uncurrying 的实现方式之一:

Function.prototype.uncurrying = function () { 
  var _this = this;
  return function() {
    var obj = Array.prototype.shift.call( arguments );
    return _this.apply( obj, arguments );
  };
};

另一种实现方法如下

Function.prototype.currying = function() {
    var _this = this;
    return function() {
        return Function.prototype.call.apply(_this, arguments);
    }
}

最终是都把this.method转化成method(this,arg1,arg2....)以实现方法借用和this的泛化

下面是一个让普通对象具备push方法的例子

 var push = Array.prototype.push.uncurrying(),
    obj = {};
  push(obj, 'first', 'two');
  console.log(obj);
/*obj {
    0 : "first",
    1 : "two"
}*/

通过uncurrying的方式,Array.prototype.push.call变成了一个通用的push函数。这样一来,push函数的作用就跟Array.prototype.push一样了,同样不仅仅局限于只能操作array对象。而对于使用者而言,调用push函数的方式也显得更加简洁和意图明了

最后,再看一个例子

var toUpperCase = String.prototype.toUpperCase.uncurrying();
console.log(toUpperCase('avd')); // AVD
function AryUpper(ary) {
    return ary.map(toUpperCase);
}
console.log(AryUpper(['a', 'b', 'c'])); // ["A", "B", "C"]
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容