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 的理解和实现 - 非网上流传的那样

你可能感兴趣的:(JavaScript:curry全局函数)