JavaScript:curry全局函数

对于柯里化的理解

  • curry 的概念:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。你可以一次性地调用 curry 函数,也可以每次只传一个参数分多次调用。

  • 这个curry全局函数,将普通的函数变成柯里化后的函数。输入是一个函数,输出也是一个函数。

假如: fn(a, b, c);       // fn是接受3个参数的函数
如果: gn = curry(fn);     // gn是fn的柯里化的等价函数
那么:
fn(a,b,c);
gn(a,b,c);
gn(a)(b)(c);
gn(a,b)(c);
gn(a)(b,c);
都是等价的,执行后的结果应该一样
  • 在参数没有给全的情况下,原函数fn和柯里化后的函数gn是不等价的。
// 有执行结果,只是参数没给全,结果可能不正常
fn(a);
fn(a,b);

// 还是函数,只是中间过程,并没有真正执行。没有执行结果。
// 输入参数被缓存了,等待继续被调用。
// 只有参数给足了,才真正执行
// 这是一种“预加载”函数的能力,通过传递一到两个参数调用函数,就能得到一个记住了这些参数的新函数。
gn(a);
gn(a,b);
gn(a)(b);
  • 在参数给多的情况下,原函数fn和柯里化后的函数gn是不等价的。有可能一样,有可能不一样。
// 参数给多了,多余的参数被忽略。和fn(a,b,c);是一样的,函数执行,结果正常
fn(a,b,c,d);
fn(a,b,c,d,e);

// 参数给多了,多余的参数被忽略。和fn(a,b,c);是一样的,函数执行,结果正常。这些情况和fn是等价的。比如
gn(a,b,c,d);
gn(a,b)(c,d);
gn(a)(b,c,d);
gn(a)(b)(c,d);

// 参数给多了,引发了error。因为函数执行之后可能已经不是函数了,再给(),就引发异常了。
gn(a)(b)(c)(d);   // gn(a)(b)(c)后函数执行,执行结果不是函数,再给(),引发异常
gn(a,b,c)(d);     // gn(a,b,c)后函数执行,执行结果不是函数,再给(),引发异常
gn(a)(b,c)(d);    // gn(a)(b,c)后函数执行,执行结果不是函数,再给(),引发异常
gn(a)(b,c,d)(e,f);    // gn(a)(b,c,d)后函数执行,d是多余参数,被忽略。执行结果不是函数,再给(),引发异常

singleCurry全局函数

  • 柯里化的提出,最基本的目的是为了简化参数形式。规定输入参数只有一个,如果参数还不够,那么就返回一个函数,将上次的输入的参数缓存起来。

  • 参数个数是有限的,每次一个,只是多写几个(),总是能给足参数的。当参数给足的时候,再调用原函数,执行,得到结果。

  • 这样可以保证输入一个参数,输出一个参数,相当于数学上的y = f(x);是最简单的形式。

  • 统一约定函数是一个输入,一个输出的最简单形式,对于函数组合,结合律等等都有好处。

  • 如果调用者给多了参数,只取第1个用就好了,其他的忽略。

  • 如果调用者没有给参数,只是给了个(),那么就返回一个新函数,加深一层嵌套而已。

gn(a,b)(c,d)(e);  // 相当于gn(a)(c)(e);就是执行了fn(a,c,e);

函数式编程入门教程
这篇文章中说的柯里化就是这种情况,每次只缓存一个参数,让函数拥有一个输入,一个输出的最简单形式。当然,输出可以是最终的执行结果,也可以是一个缓存了历史输入参数的函数。

placeholderCurry全局函数

  • 一开始没想好,要给什么参数,先给个占位符,比如_

  • 虽然参数个数够了,不过其中有占位符_的话,仍然不执行,等待继续输入

  • 继续输入,替换缓存的占位符,如果参数够了,就执行

gn(a, _, c)(d,e);  // 相当于gn(a)(d)(c)(e);多余的e被加在最后;就是执行了fn(a,d,c);多余的e被忽略
  • 由于依赖外部变量_,所以placeHolderCurry这个函数是“不纯的”。不过通过module.exports包装一下,又可以变成不依赖外部状态的“纯函数”

  • ES6,对象的keyvalue如果是相同的名称,那么可以合并起来写,方便一点

// 以下两者是等价的
{a : a, b : b, c : c}
{a, b, c}

如何实现?

  • 收集参数,暂存在一个数组中,先输入的参数在前,后输入的参数在后

  • 一个函数需要的参数个数是知道的,比如,函数fn的参数个数是fn.length。这个就是原始函数执行的条件。

  • 如果函数参数足够了,那么就执行原始函数fn,如果参数还不足,那么就返回一个新函数,缓存已经输入的参数,等待接收新参数。

  • 每次输入的参数个数是不确定的。原始函数fn需求的函数参数的具体个数也是不确定的。所以需要用到“递归思想”

  • 返回一个新函数,在函数层级上又嵌套一层,包含的更深了。如果原始函数fn的参数比较多,并且每次输入的参数比较少,那么函数嵌套的层次还是比较多的。还原出来,还是比较难看的。

  • 函数都有一个内部的arguments,用来实际保存输入的参数。需要用这个特性。这个arguments是个类数组,有indexlength,但是不是数据。

  • ES6之后,提供了剩余参数功能fn(...args)这个args是个正真的数组。如果方便,可以考虑用这个。

  • 记忆参数,可以放在一个数组中。这个数组可以放在递归函数外面,也可以放在递归函数的参数中。相比较之下,还是放在递归函数的参数中比较合适。

  • 另外,递归函数是执行函数,还进一步缓存参数,需要记忆一个原始参数的个数。因为递归函数退出时要执行原始函数,所以将这个原始函数当做递归函数的参数是比较合理的。

实现代码

文件名:curry.js

const _ = {} // placeholder

// 每次可以输入一个或者多个参数
function curry(fn) {
    return recursiveCurry(fn, []);
}

// 递归调用,缓存输入参数,或者执行原始函数fn
function recursiveCurry(fn, args) {
    if (isArgumentsReady(fn, args)) {
        return fn.apply(this, args);
    } else {
        return function(...newArgs) {
            return recursiveCurry(fn, concatArguments(args, newArgs));
        };
    }
}

// 每次只取一个参数,多余参数忽略,()直接忽略
function singleCurry(fn) {
    return recursiveSingleCurry(fn, []);
}

// 递归调用,缓存输入参数,或者执行原始函数fn;每次只取一个参数
function recursiveSingleCurry(fn, args) {
    if (isArgumentsReady(fn, args)) {
        return fn.apply(this, args);
    } else {
        return function(...newArgs) {
            var parameters = [];
            const firstArgument = newArgs[0];
            if (firstArgument) {
                parameters = [firstArgument];
            }
            return recursiveSingleCurry(fn, concatArguments(args, parameters));
        };
    }
}

// 每次可以输入一个或者多个参数,还可以输入占位符
function placeholderCurry(fn) {
    return recursivePlaceholderCurry(fn, []);
}

// 递归调用,缓存输入参数,或者执行原始函数fn;每次只取一个参数
function recursivePlaceholderCurry(fn, args) {
    if (isArgumentsReady(fn, args, _)) {
        return fn.apply(this, args);
    } else {
        return function(...newArgs) {
            return recursivePlaceholderCurry(fn, concatArguments(args, newArgs, _));
        };
    }
}

// private
function isArgumentsReady(fn, args, placeholder) {
    if (placeholder) {
        // 占位符没有处理完,继续等待输入
        if (args.indexOf(placeholder) !== -1) {
            return false;
        }
        // 参数个数还不够,继续等待输入
        if (args.length < fn.length) {
            return false;
        }
        // 没占位符,个数也够了
        return true;
    } else {
        return args.length >= fn.length;
    }
}

function concatArguments(oldArguments, newArguments, placeholder) {
    if (placeholder) {
        // 替换占位符
        var i = 0;
        const replaceArguments = oldArguments.map(function(argument) {
            if (argument === placeholder && i < newArguments.length) {
                return newArguments[i++];
            } else {
                return argument;
            }
        });
        // 有多余参数,添加到尾部
        if (i < newArguments.length) {
            return replaceArguments.concat(newArguments.slice(i));
        } else {
            return replaceArguments;
        }
    } else {
        return oldArguments.concat(newArguments);
    }
}

module.exports = {
    singleCurry,
    curry,
    placeholderCurry,
    _,
};

测试代码

文件名:curry_test.js,与实现文件curry.js在同一目录。

const curry = require('./curry.js');
const log = console.log;

// 为了测试判断简单,只是将参数变成数组输出
const fn = function(a, b, c) { 
    var array = [a, b, c];
    log(array);
    return array; 
};

const cfn = curry.curry(fn);
cfn("a", "b", "c");   // [ 'a', 'b', 'c' ]
cfn("a","b")(5,"d");  // [ 'a', 'b', 5 ]
cfn("a", "b")("c");   // [ 'a', 'b', 'c' ]
cfn(1)(2, "c");       // [ 1, 2, 'c' ]
cfn("a")(6)()("c");   // [ 'a', 6, 'c' ]

const sfn = curry.singleCurry(fn);
sfn(1)(2)(3);              // [ 1, 2, 3 ]
sfn(1,2,3)()(4,5)(1);      // [ 1, 4, 1 ]; 多余的参数被忽略;()被忽略
var temp = sfn(1,2,3)();   // 1, 参数不够
temp(1)(4,5,6);            // [ 1, 1, 4 ]
sfn('a')('b', 'c')(5,6);   // [ 'a', 'b', 5 ]

const pfn = curry.placeholderCurry(fn);
pfn("a", curry._, "c")("b");                   // [ 'a', 'b', 'c' ]
pfn(curry._, "b")("a")("c");                   // [ 'a', 'b', 'c' ]
pfn(curry._, "b",curry._)("a","c");            // [ 'a', 'b', 'c' ]
var temp = pfn(curry._, "b", curry._)("a");    // 'a' , 'b', _ 参数不够
temp(3,4,5);                                   // [ 'a', 'b', 3 ]
pfn("b", curry._)("a")(1);                     // [ 'b', 'a', 1 ]

参考文章

JavaScript 函数式编程中的 curry 实现

深入解析JavaScript中函数的Currying柯里化

js 中 curry 的理解和实现 - 非网上流传的那样

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

推荐阅读更多精彩内容