【基于 JS 函数式编程-3】柯里化 | 偏函数 | 组合与管道

⭐️ 本文首发自 前端修罗场(点击即可加入),一个专注 Web 技术、答疑解惑、面试辅导、职业发展的社区。

相关文章

❤️ 现在关注【前端修罗场】,后台回复【666】,即可获取一份【免费的优质学习资料】,一起学习,一起进步~


如题,理解柯里化和偏应用,能帮助我们在函数式组合中进行应用。

概念

一元函数

定义: 只接受一个参数的函数,称为一元函数。如:

const fn = (x)=>x;

二元函数

定义:接受两个参数的函数,称为二元函数。如:

const add =(x,y)=>x+y;

变参函数

定义:接受可变数量参数的函数,称为变参函数。
在es5中我们可以通过arguments来捕获调用变参函数的额外参数。
在es6中,我们可以使用扩展运算符:"..."实现变参函数。如:

const varfn = (a,...varparms)=> {
	console.log(a);
	console.log(varparms);
}
varfn(1,2,3); // 1 , [2,3],我们把[2,3]称为额外参数

柯里化

定义:柯里化(Curry,以数学家Haskell Curry命名),常被翻译为“局部套用”,是把一个多参函数转换为一系列单参函数并进行调用的过程。

柯里化允许我们把函数与传递给这个函数的参数相结合,产生出一个新的函数。

如:下列代码中,add1是把1传递给add函数的curry方法后创建的一个新函数。

let add1 = add.curry(1);
console.log(add1(3));

再如:

const add =(x,y)=>x+y; //二元函数
进行柯里化:
const addCurry = x => y=>x+y;
addCurry(2)(3); //5

但是,Javascript自己并没有Curry方法。我们可以通过给Function.prototype扩展此功能:

Function.method('curry',function(){
	let slice = Array.prototype.slice,
		args = slice.apply(arguments),
		//arguments并非真正的数组,没有concat方法,要避开这个问题,
		//我们必须在两个arguments数组上应用数组的slice方法。
		//这样能产生出拥有concat方法的常规数组
		that = this;
		return function() {
			return that.apply(null,args.concat(slice.apply(arguments)));
		}
})

curry函数定义

const curry = (binaryFn)=> {
	return function(firstArg) {
		return function(secondArg){
			return binaryFn(firstArg,secondArg);
		};
	};
};
let autoCurriedAdd = curry(add) //通过curry函数把add函数转换为一个柯里化函数
autoCurriedAdd(2)(3); //5

但是,有人会问:柯里化有什么用处呢

因为有时候我们可能想把多个函数及带有多个参数的函数柯里化,所以,下面我们重构一下curry函数

let curry = (fn)=> {
	if(typeof fn!=='function') {
		throw Error('No function provided!');
	}
	return function curriedFn(...args) {
		if(args.length<fn.length){ //检查通过...args传入的参数长度是否小于函数参数列表的长度。如果是,进入if,否则调用整个函数。
			return function() {
				return curriedFn.apply(null,args.concat([].slice.call(arguments)));//使用concat函数连接一次传入一个的参数,并递归调用curriedFn。
				//除此之外,由于args是类数组,并没有concat方法,
				//所以,需要应用数组的slice方法。
			};
		}
		return fn.apply(null,args);//直接调用整个函数
	};
};
const multiply = (x,y,z) =>x*y*z;
curry(multiply)(3)(2)(1);//6

偏应用

偏函数(partial)

const partial = function(fn,...partialArgs) {
	let args = partialArgs;//捕获传入函数的参数 args= [undefined,10]
	return function(...fullArguments) {//闭包函数,接受一个fullArguments的参数
	//fullArguments指向 console.log('1');
		let arg = 0;
		for(let i = 0;i < args.length&& arg<fullArguments.length;i++) {
			if(args[i] === undefined) {
				args[i] = fullArguments[arg++];
			}
		}
		return fn.apply(null,args);
	};
};
let delayTenMs = partial(setTimeout,undefined,10);
delayTenMs(()=>console.log('1'));

我们可以将partial函数应用于任何含有多个参数的函数。如:

let obj = {foo:"xxx",bar:"yyy"};
JSON.stringify(obj,null,2);
转换为应用偏函数:
let prettyPrintJson = partial(JSON.stringify,undefined,2);
prettyPrintJson({foo:"xxx",bar:"yyy"});//"{"foo":"xxx","bar":"yyy"}"

上面我们说了柯里化和偏函数,但是需要注意的是:

柯里化和偏函数并不是同时需要。这主要取决于API是如何定义的。如果API如,map、filter一样定义,我们可以使用curry函数解决问题。但是,如果不是为curry函数设计的函数,如setTimeout,有时填充函数的前两个参数和最后一个参数会使中间的参数处于一种未知状态(undefined)!我们选择partial更合适!

组合与管道

概念

在Unix中有这么一套思想:
1、每个程序只做好一件事情。为了完成一项新的任务,重新构建要好于在复杂的旧程序中添加新”属性“。在函数式编程中,”接受一个参数并返回数据“正是遵循了该条思路。
2、每个程序的输出应该是另一个尚未可知的程序的输入。

管道

管道允许我们通过组合一些函数去创建一个能够解决问题的新函数。

如图:

在这里插入图片描述

管道在两个函数之间扮演了桥梁的角色。

函数式组合

如下示例代码:

map(filter(arg,(item)=>item.rating[0]>4.5),(item)=>{
	return {}
})

我们看到,上面代码中filter输出的数据被作为输入参数传递给map函数。
这种创建一个函数,通过把一个函数的输出作为输入发送给另一个函数的方式把两个函数组合起来,我们称为函数式组合。组合的思想,就是把小函数组合成一个大函数。
示例:

//compose函数
const compose =(a,b)=> {
	(c)=>a(b(c)) //b的输出作为a的输入
}
let number = compose(Math.round,parseFloat);
number("3.56");//4

compose函数会首先执行b,并将b的返回值作为参数传递给a。该函数调用的方向是从右至左的,即先执行b,再执行a。

管道/序列

从左至右处理数据流的过程称为管道(pipeline)或序列。

//pipe函数,compose函数的复制品,修改了数据流
const pipe = (...fns) => 
	(value) => 
		reduce(fns,(acc,fn)=>fn(acc),value);

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编程轨迹_

期望和你分享一杯咖啡

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值