相关文章
【函数式编程】基于JS 进行函数式编程(一)引入 | 什么是函数式编程 | 函数式编程的优点
【函数式编程】基于JS进行函数式编程(二)高阶函数 | 函数代替数据传递 | 函数是一等公民 | 闭包 | 使用高阶函数实现抽象 | 数组的高阶函数
【函数式编程】基于JS进行函数式编程(三)柯里化 | 偏函数 | 组合与管道
【函数式编程】基于JS进行函数式编程(四)函子 | MayBe函子 | Monad函子
如题,理解柯里化和偏应用,能帮助我们在函数式组合中进行应用。
概念
一元函数
定义: 只接受一个参数的函数,称为一元函数。如:
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);