柯里化currying与偏函数partial

292 阅读3分钟

偏函数(partial function)

偏函数就是将一个 n 参的函数转换成固定 x 参的函数,剩余参数(n - x)将在下次调用全部传入

function add(a, b, c){ 
    return a + b + c 
} 
add(1, 2, 3) //6

这是一段普通函数,但是如果我们让它的某个参数固定(默认是从第一个参数固定),后续参数重新扩展传递给原函数,那么它就是一个偏函数

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

// 假设有一个 partial 函数可以做到局部应用
var addOne = partial(add, 1);

addOne(2, 3) //6

柯里化(currying)

柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

function add(a, b, c){ 
    return a + b + c 
} 
add(1, 2, 3) //6

// 假设有一个 curry 函数可以做到柯里化
var addCurry = curry(add);

addCurry(1)(2)(3) // 6

柯里化的作用

在我们了解柯里化的定义后,那么它实际的运用场景以及意义是什么一定是我们最关注的。

举个例子:

// 示意而已
function ajax(type, url, data) {
    var xhr = new XMLHttpRequest();
    xhr.open(type, url, true);
    xhr.send(data);
}

// 虽然 ajax 这个函数非常通用,但在重复调用的时候参数冗余
ajax('POST', 'www.test.com', "id=1")
ajax('GET', 'www.test.com', "id=2")
ajax('PUT', 'www.test.com', "id=3")

// 利用 curry
var ajaxCurry = curry(ajax);

// 以 POST 类型请求数据
var post = ajaxCurry('POST');
post('www.test.com', "id=1");

// 以 POST 类型请求来自于 www.test.com 的数据
var postTestService = post('www.test.com');
postTestService("id=1");

curry 的这种用途可以理解为:参数复用。本质上是降低通用性,提高适用性。

这种用途依旧显得比较片面,那我们再举个例子,用它生成的函数传给高阶函数,比如map

举个例子:

比如我们有这样一段数据

var person = [{name: 'kevin'}, {name: 'daisy'}]

我们要将里面所有的name值提取出来,我们可以这样做

vae name = person.map((item)=>{return item.name})

如果我们有curry函数

var prop = curry(function(key,obj)=>{
    return obj[key]
})

var name = person.map(prop('name'))

虽然说我们为了获取name又编写了一个prop函数,但其实这个prop函数是可以复用的,并且代码更加易懂了,一眼就能知道我们要用person遍历获取name属性

接下来我们就来实现这个curry函数

function curry(fn) {
    var length = fn.length;

    var args = [].slice.call(arguments,1) || [];
    
    return function() {

        var _args = args.slice(0),

            arg, i;

        for (i = 0; i < arguments.length; i++) {

            arg = arguments[i];

            _args.push(arg);

        }
        if (_args.length < length) {
            return curry.call(this, fn, _args);
        }
        else {
            return fn.apply(this, _args);
        }
    }
}

var fn = curry(function(a, b, c) {
    console.log([a, b, c]);
},'a','b');
fn("c")   //[a,b,c]

-----------------------------------------------------------------------

fn = curry(function(a, b, c) {
    console.log([a, b, c]);
});
fn("a","b",""c) //[a,b,c]
fn("a","b")("c") //[a,b,c]
fn("a")("b")("c") //[a,b,c]

我们拿fn("a")("b")("c")举例

以下 function(a, b, c) { console.log([a, b, c]); }由func代替

fn("a")("b")("c")相当于 curry(func)("a")("b")("c")

执行curry(func) 得到curry(func,"a")("b")("c")=>curry(func,"a")("b")("c")

=>curry(func,"a","b")("c") => curry(func,"a","b","c") => func("a","b","c")

看的出来,除了最后一步,每次执行都是生成一个偏函数

偏函数的实现

那我们回归到偏函数 试着实现一个偏函数。

也许我们可以直接用bind?来让我们试一试

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

var addOne = add.bind(null, 1);

addOne(2) // 3

然而使用 bind 我们还是改变了 this 指向,我们要写一个不改变 this 指向的方法。

// 似曾相识的代码
function partial(fn) {
    var args = [].slice.call(arguments, 1);
    return function() {
        var newArgs = args.concat([].slice.call(arguments));
        return fn.apply(this, newArgs);
    };
};

我们来写个 demo 验证下 this 的指向:

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

// var addOne = add.bind(null, 1);
var addOne = partial(add, 1);

var value = 1;
var obj = {
    value: 2,
    addOne: addOne
}
obj.addOne(2); // ???
// 使用 bind 时,结果为 4
// 使用 partial 时,结果为 5

柯里化与偏函数的区别

大家应该都能发现,柯里化与偏函数其实非常相似,所以两者到底有什么区别呢:

柯里化是将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数。

局部应用则是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数。

如果说两者有什么关系的话,引用 functional-programming-jargon 中的描述就是:

Curried functions are automatically partially applied.

值得注意的是:underscore 和 lodash 都提供了 partial 函数,但只有 lodash 提供了 curry 函数。