[函数式编程-回顾] 柯里化的实现

827 阅读7分钟

柯里化原理

把一个多参函数转化为单参函数

输入输出

输入

一个函数

输出

两种情况:

  1. 输入函数的参数没有传齐时都是返回 一个函数。
  2. 输入函数的参数传齐时,返回输入函数的执行结果。

注意点

函数的length属性

返回函数参数的长度

function a(a,b,c){
	return a+b+c;
};
a.length; // 3

实现

1. 普通函数的另外一种调用方式

改变了传参的方式(参数都传齐时,直接调用fn函数,并把返回函数的参数给到fn)

针对的是,直接在curry包装后的函数,把参数都传给他,在本函数内,就是做了一个参数的传递,把curry(fn)(...args),中的args传递给fn函数。

function curry(fn){
	if(fn.length <=1){return fn}
	var returnFn = (...args) => {
		if(fn.length === args.length){
			//参数都传齐时,直接调用fn函数,并把返回函数的参数给到fn
			return fn(...args)
		}else{
			//参数没传齐时,打印传入的参数。
			console.log(args)
			
		}
	}
	return returnFn
}
var add = (a,b,c,d) => a+b+c+d;
var curriedAdd = curry(add);
curriedAdd(1,2,3); // 打印出 [1, 2, 3]

分析

  • curry后返回一个函数。
  • 给返回的这个函数传递参数
  • 当returnFn的参数个数与输入函数fn的参数个数相同时
  • 调用fn函数并把returnFn的参数给到fn。
  • 返回上一步的调用结果。也就是相当于换了一种方式调用输入函数。
add调用方式对比:

普通函数调用:

var add = (a,b,c,d) => a+b+c+d;
add(1,2,3,4)

用curry包装后的调用方式

var add = (a,b,c,d) => a+b+c+d;
var returnFn = curry(add);
returnFn(1,2,3,4)

  1. 不直接调用add。而是把add当做一个参数,传给另外一个函数curry。
  2. 返回一个函数returnFn。
  3. 把参数传给返回的函数。
  4. 在返回的函数内部返回调用的了fn。
  5. 整体相当于curry(add)(1,2,3,4) , curry第一个参数是我们要调用的函数。返回函数的参数是我们要调用函数的参数。

2. 完整版的柯里化(es6的版本)(递归+参数拼接)

参数的拼接方式,比较灵活。建议使用此方法

function curry(fn){
	if(fn.length <=1){return fn}
	var returnFn = (...args) => {
		//console.log(...args,typeof ...args);
		if(fn.length === args.length){
			//参数都传齐时
			return fn(...args)
		}else{
			//参数没传齐时,就返回一个函数
			return (...args2) => {
				console.log(args2,"     ",args);
				return returnFn(...args,...args2)
			}
			
		}
	}
	return returnFn
}
var add = (a,b,c,d) => a+b+c+d;
//包装add
var returnFn = curry(add);
// 递归传递参数给returnFn
var returnFnArrowFn = returnFn(1)(2)(3);
// 参数传齐,returnFn将参数传递给输入函数fn, 并调用fn
returnFnArrowFn(4);// 10

递归调用 returnFn 时的参数传递

如上代码中的 console.log(args,args2) 的打印结果

console.log(args,args2);
// [1]        [2]
// [1,2]      [3]
// [1,2,3]    [4]
// 此时,参数也传齐,走上面的if分支。调用输入函数fn。

args 是 returnFn的参数。 args2 是 returnFn 中匿名函数的参数。

分析

  1. (参数<=1时,原样输出)判断输入函数的参数个数,如果参数个数<=1,那么直接返回输入函数
  2. 定义返回的函数,并返回
  3. (返回的函数把参数传齐时)返回函数的入参个数判断,如果入参个数等于输入函数fn的参数个数,那么,说明参数都已传齐,直接调用输入函数fn,并把返回函数的参数传给输入函数。(因为我们最终的目的就是调用输入函数,参数当然也都是给fn用的)
  4. (参数没传齐时),永远都是返回一个函数。
  5. 返回的这个函数的参数在传递给returnFn函数。
  6. 递归调用returnFn函数
  7. 直到参数都从最里层的返回函数传递给returnFn函数。returnFn函数的参数在传递给输入函数。
  8. 注意:只有return函数的参数个数和输入函数的参数个数相同时,才会传递给输入函数。并调用输入函数,并返回。

画图分析

3. 实现方式三(es5的版本)(递归+参数拼接)

es5的版本,参数拼接方式比较繁琐。

function curry(fn, args=[]) {// 参数args不建议传入
    length = fn.length;
   // args = args || [];

    return function() {
        //复制curry中的第二个参数数组到_args
        var _args = args.slice(0),
        arg, i;
        for (i = 0; i < arguments.length; i++) {
            arg = arguments[i];
            // 把返回函数的参数 push 到 _args中。
            // _args用于存储 所有的 不断传入的参数。
            _args.push(arg);
        }
        if (_args.length < length) {
            // 当参数不齐时,递归调用自身,并把现有的拼接好的参数传递给curry。
            // 直到 参数齐时,调用输入函数fn,并传入所有拼接好的参数。
            return curry.call(null, fn, _args);
        }
        else {
            // curry函数的最后执行的一步,参数已传齐,开始调用fn。
            return fn.apply(null, _args);//  匿名函数中的this。指向的是window。没必要修改this。
        }
    }
}


var returnFn = curry(function(a, b, c) {
    return a+b+c;
});

var nextFn = returnFn(1)(2);// 参数不齐,返回匿名函数,收集参数
nextFn(3); // 6     // 参数已齐,调用curry,的fn,真正求值。

分析

  • 借助curry函数的第二个参数来,不断的收集后续传入的参数。
  • 借助_args变量来连接参数和新传入的参数
  • 递归调用curry,不断的传入连接好的参数。
  • 直到,_args.length === length, 传入的参数总数和curry包装的函数fn的参数个数相同时
  • 调用fn,并传入所有连接好的参数。
  • 返回fn的执行结果。

参考

应用场景

  • ajax
  • 代码简化
  • ramdajs, underscorejs, lodash 等工具库都使用了函数式的思想
  • 比如,pipe, compose, cond, R.ifElse , 等等

ajax的柯里化场景

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

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

// 虽然 ajax 这个函数非常通用,但在重复调用的时候参数冗余
ajax('POST', 'www.andy.com', "name=andy")
ajax('POST', 'www.andy.com', "name=andy")
ajax('POST', 'www.andy.com', "name=andy")

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

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

// 以 POST 类型请求来自于 www.andy.com 的数据
var postFromTest = post('www.andy.com');
postFromTest("name=andy");

对象取值的柯里化场景

普通写法

var person = [{name: 'andy'}, {name: 'amy'}];

我们取person中的name,代码怎么写呢?

var nameArr = person.map((item,index) => {
    return item.name
});
console.log(nameArr); // ["andy", "amy"]

通过ramdajs 我们怎么写呢?

var person = [{name: 'andy'}, {name: 'amy'}];

R.map(R.prop('name'))(person); // ["andy", "amy"]

通过 Lodash 如何取呢?

var person = [{name: 'andy'}, {name: 'amy'}];
_.map(person,'name'); // ["andy", "amy"]

通过 underscore 如何取呢?

var person = [{name: 'andy'}, {name: 'amy'}];
_.map(person,(item)=>item.name); // ["andy", "amy"]
或者:
_.map(person,['name']); // ["andy", "amy"]

以上代码ramdajs 和 lodash,underscore。的区别,明显的是数据的传递方式不一样。 ramdajs是数据传递放后面。而后两者是数据传前面。

柯里化用途小结

  1. 以上第一个例子,是参数复用
  2. 第二个例子是,取数组对象中的某个属性,把三行代码简写成一行代码
  3. R.map(R.prop('name'))(person); 代码的可读性与提高了。翻译成中文是,person对象(map)遍历,取属性(prop) 'name'

就像读英文句子一样去读代码。

总结

使用了几年的函数式库,把内部的重要理念形成文章。回顾一下,也希望对读到文章的同学有所用。共同深入。

  1. 柯里化,直白的说,就是包装一个普通函数,换另外一种方式调用这个普通函数
  2. 普通函数被curry化后,将更加灵活。复用性更强
  3. 柯里化的好处一:参数复用
  4. 还有函数式的其他特点:高阶函数,纯函数(幂等函数),后续我们在一一介绍。
  5. 提高代码的可测试性,可读性,代码的健壮性。

curry的实现,最关键的就是 返回函数 + 递归 + 参数的传递 + 真正调用输入函数