函数的编写:纯函数
函数的组合产生更大的威力: 管道组合(高阶函数 闭包 偏函数 柯里化)
函数的语义化: 链式调用(函子)
函数的编写:纯函数
函数式编程第一要求即无副作用,即函数内部的运算不可以更改函数外部的值,这就是纯函数的概念:对于一个纯函数,相同的输入,总会得到相同的输出。
为什么要使用纯函数?利于测试,减少程序复杂度从而降低bug率
函数的组合产生更大的威力: 函数组合(管道 高阶函数 闭包 偏函数 柯里化)
多个不同功能的纯函数,通过组合可以实现不同的业务逻辑,通过将他们组合起来实现一个更大范围的偏向业务的纯函数。比如:
function sum(a, b) { return a + b; }
function reduce(a, b) { return a - b; }
上面是两个纯函数,只能实现简单的加减算法,但是在实际的业务场景中不可能是如此简单,有可能我们需要先进行加法运算,然后再把加法运算的结果 与另外一个结果进行减法运算。
即:
let a = sum(1, 2);
let b = reduce(a, 3);
这是声明式的写法,一旦这种场景增多,我们总是希望可以将上述过程给封装起来,方便复用,同时利于测试。
let b = reduce(sum(1, 2), b);
上面应该不算是封装,而是简单的组合,这在函数非常少的时候,可以这样写,一旦函数增加到5个以上,将使得代码阅读变得非常困难,于是催生出一系列关于函数组合的方式:
1:管道
管道顾名思义就是将多个函数比喻组成一个管道,然后让数据按照顺序一次通过这些函数,最终得到结果,管道的实现如下:
var compose = function(a, b) {
return function(c) {
return a(b(c));
}
}
let m = compose(reduce, sum);
let b = m(1, 2);
上面就是一个组合,我们最终得到一个函数m,函数m的功能是完全与业务耦合的,这是一个业务的纯函数,但是这里有一个问题,即我们的compose里面的函数 都是只接受一个参数的,但是我们的函数却是两个参数,该如何解决这种问题?
这里涉及到一个概念,即函数的柯里化,这里先不说什么是柯里化,在我们上面遇到的问题中,我们会发现 函数组合的前提是被组合的函数必须只能接受一个参数,多余的参数会丢失,但是我们日常用的函数大部分都是多个参数,所以我们必须想到一种方式将多个参数的函数转换成多个一个参数的函数:
function(a, b) { return a + b; }
柯里化后:
function curry(a) {
return function(b) {
return a + b;
}
}
最终我们得到,两个一元函数:
var m = curry(1);
var n = m(2);
然后这种方式还是太过于麻烦,复杂,还有另外一种方式,偏函数:
偏函数是指先固定函数的多个参数,然后只留下最后一个参数不传入,然后就变成了一个一元函数,这类似于bind:
var partialSum = sum.bind(num, 2);
var m = partialSum(1);
然而使用bind会改变this的指向,我们优化一下:
function partial(fn) { var args = [].slice.call(arguments, 1); return function() { var newArgs = args.concat([].slice.call(arguments)); return fn.apply(this, newArgs); }; };
验证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
柯里化和偏函数都是函数组合的辅助,我们看一下 偏函数如何在函数组合中运用:
偏函数的使用场景在于参数的灵活变换可以得到不同的结果,比如我们有一个函数,传入不同的参数可以得到不同的结果,
function sum(a, b, c) { return a + b + c }
我们根据业务需求需要分别计算:1+1+1 和2+2+2,然后将结果运用于另外一个函数C
所以我们可以使用偏函数得到两个函数:
var p1 = partial(sum, 1, 1);
var p2 = partial(sum, 2, 2);
var result1 = compose(C, p1);
var result2 = compose(C, p2);
在上面柯里化和偏函数中其实我们使用了闭包。
那高阶函数呢?
高阶函数是指,把函数作为参数传入到另外一个函数中,从而达到将计算行为权限外放的目的,比如说:数组循环求和,这里有两个步骤,一个是循环,一个是求和,在我们编写代码中 会存在无数次循环的场景,但是循环后坐什么 确是不一样的,所以我们可以把循环的行为抽离,然后将循环后的计算外放,从而达到代码抽离的作用,本质上是将一个函数的运算分解为多个步骤,然后将其中重复的抽离出来,从而达到复用的目的。
函数的语义化: 链式调用(函子)
然而使用函数组合依然无法面对所有的业务场景,业务场景总是多变的,我们不可能遇见到所有的场景,并将这些业务场景使用函数组合描绘出来,这个时候就需要有一种更加灵活的方式来组合函数之间的关系,即链式调用:
我们依据自己的需求,灵活的挑选函数来使用,并一次调用,比如:
var m = A(1).B().C().getResult();
我们在明确的知道A B C 函数的功能后,将他们依次调用,从而求解,这样相比声明式代码更加的语义化,如何实现链式调用呢?
最简单的方式类似与jquery:
var container = {
val: 1,
add: function(a) {
this.val = this.val + a;
return this;
},
reduce: function(a){
this.val = this.val - a;
return this;
}
}
var m = container.add(5).reduce(2);
这是最简单的实现链式调用的方式,但是这里有一些缺点,
1:我们把函数放在对象之中,即函数本身有了从属,我们在使用函数前,必须引用函数所在的对象,而从引用了我们不需要的其他函数
2:这里并不是纯函数,函数内部更改了外部的值
我们希望的链式调用应该是这样的:
function sum(a, b){ return a + b }
function reduce(a, b){ return a - b }
var m = sum(1, 2).reduce(3, 4).getResult();
函数的随意组合才是我们希望得到的链式调用,而函子可以解决这个问题:
class Functor {
constructor(val) {
this.val = val;
}
of() {
return new Functor(val);
}
map(f) {
return new Functor(f(this.val));
}
getResult() {
return this.val;
}
}
var m = Functor(2).map(sum).map(reduce).getResult();
我们通过函子实现了不同函数的随意组合,这里唯一的缺陷就是只可以传入接受一个参数的函数,我们需要在调用前 将多元函数转化为一元函数,这意味着我们在使用多元函数前必须要先做一步处理才能使用链式调用。在链式调用中上一个函数的结果是下一个函数的参数,链式调用适应与对于单一数据的处理,即一个数据经过多道工序最后加工成另外一个产品,本质上类似于车间里面的一道道工序,所以他并不适应于多个参数,那本身就是不纯的。
进一步谈函子
函数式思想:
所以函数式编程并不意味着在任何场景下都适应。明白函数式编程思想,在需要的时候使用他,才不会滥用。
所以在数据的工序处理,我们可以使用链式调用,同时将多个单一功能函数组合,我们可以得到一个复杂的纯函数(这样做的前提是在其他场景也会用到,可以复用),同时利用高阶函数将多个函数公用部分进行抽离,下面是具体的函数式编程应用场景:
1:尾递归优化(柯里化应用) https://www.cnblogs.com/freelandun/p/7290768.html
2:map reduce函数(高阶函数引用)
3:函数柯里化 如loadsh中curry的应用
如何在项目中运用函数式编程?
运行函数式编程的目的有三个:
1:纯函数 为了减少副作用,减少bug率
2:函数组合 柯里化 惰性求值
3:链式调用 为了语义化
1:所以我们应该先选一个函数库比如loadsh或者ramda作为一个基础的纯函数库,然后我们会根据我们的业务封装出一些比较大的纯函数用来实现具体业务功能
2:在具体的代码编写过程中,我们可以尝试将多个lodash函数组合起来以发挥更大的威力,他与1的区别在于更加灵活。
3:通过链式调用和函子,我们可以写出非常优美的代码和清晰的数据流。
参考:
https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/ch1.html#%E4%BB%8B%E7%BB%8D