函数式编程四特征

本文分为五个部分:

  1. 为什么要用函数式编程
  2. 函数式编程的概要
  3. 函数式编程的四特征
  4. 函数式编程四特征总结

一,为什么要用函数式编程

面向对象的一些特点

  1. 某些场景代码充斥着无数的for循环,以及无数的分支判断语句,使代码的可读性,健壮性降低
  2. 某些场景代码定位问题比较困难,因为某些方法的每次调用输出都不一样,并且会改变输入数据,(数据经常被一些不纯函数无意间修改)导致代码出错率增加
  3. 函数使用不够灵活,不能自由组合,复用率低

二,函数式编程概要

注:本文用到的函数式库是ramda,类似函数式库有underscore,lodash等
通过此图我们先大概看一下函数式编程的函数组合的特性

接下来我们具体看一下函数式编程

函数式思想-λ演算

函数式思想展现的是一种纯粹的数学思维。来源于Lambda演算 ( λ ) (演算是一套用于研究函数定义、函数应用和递归的形式系统)
函数并不代表任何物质(对象,相对于面向对象思想而言),而它仅仅代
表一种针对数据的转换行为。它们是对行为的最高抽象,具有非凡
的抽象能力和表现力。函数式编程, 就是用函数(计算)来表示程序, 用函数(计算)的组合来表达程序的组合的思维方式.

函数式编程的本质

  1. 编程任务 (一个或多个这样的任务,就组成了程序)

编程主要就是写中间的那部分运算逻辑,如图:

现在,主流写法是过程式编程和面向对象编程,还有擅长纯运算函数式编程(强调将计算过程分解成可复用的函数)

  1. 我们可以把整个运算过程,想象成一根水管(pipe),数据从这头进去,那头出来,如图:

函数式编程思维, 就是用函数(计算)来表示程序, 用函数(计算)的组合来表达程序的组合的思维方式.

三,函数式编程的四特征

FP特点一 纯函数

纯函数是指只处理传入的参数。

  1. 一定有返回值(返回一个新的值,不改变原始数据)
  2. 至少传入一个参数
  3. 没有副作用,输入相同,保证输出相同(幂等函数)

javascript内置函数有不少纯函数,也有不少非纯函数。

纯函数:
1
2
3
4
5
1. Array.prototype.slice

2. Array.prototype.map

3. String.prototype.toUpperCase
非纯函数:
1
2
3
4
5
1. Math.random

2. Date.now

3. Array.ptototype.splice

我们看一下非纯函数 splice 和 纯函数 slice

1
2
3
4
5
//splice 是典型的非纯函数,每次的返回值不一次,而且会把输入数据改变
var r1 = [1,2,3,4,5,6,7];
r1.splice(0,3);// 输出[1,2,3] 此时r1为[4, 5, 6, 7]
r1.splice(0,3);// 输出[4, 5, 6] 此时r1为[7]
r1.splice(0,3);// 输出[7] 此时r1为[]
1
2
3
4
5
//slice 是纯函数,每次的返回值都一样,而且会把输入数据不会被改变
var r=[1,2,3,4]
r.slice(0,3);//输出 [1, 2, 3] 此时 r 为  [1, 2, 3, 4]
r.slice(0,3);//输出 [1, 2, 3] 此时 r 为  [1, 2, 3, 4]
r.slice(0,3);//输出 [1, 2, 3] 此时 r 为  [1, 2, 3, 4]

这就是我们强调使用纯函数的原因,因为纯函数相对于非纯函数来说,在可缓存性、可移植性、可测试性以及并行计算方面都有着巨大的优势。

FP特点二 不可变

函数式编程中 (不可变数据使代码更简单和安全)

  1. 没有变量 由于历史原因存储的值仍然叫变量,但他们是不可变的。比如说,一旦 x 中存了一个值,它的生命周期中就一直是这个值;
  2. 没有循环 通过递归来做循环 非递归的循环更容易出问题,因为需要使用可变变量
1
2
3
4
5
6
//通过for来求和,使用可变变量i,是存在一定风险的。
var acc = 0;
for (var i = 1; i <=10; ++i){
acc += i;
}
console.log(acc);//55

当然我们也可以使用let来规避,但其实也是存在风险的

1
2
3
4
5
6
7

var acc = 0;

for (let i1 = 1; i1 <=10; ++i1){ i1=111
acc += i1;
}
console.log(acc);//111
1
2
3
4
5
6
//函数式当中递归的处理方式,很显然,此函数只要输入相同,输出就一定相同,不存在可变变量。
function sumRange(start, end, acc){
if (start > end) return acc;
return sumRange(start + 1, end, acc + start)
}
console.log(sumRange(1, 10, 0));//55

还有一种不错的,用js内置的reduce来处理,看上去好了很多。这是js趋向于函数式的一种体现。

1
2
3
4
5
var arr = [0,1,2,3,4,5,6,7,8,9,10]; 
//reduce 数组成员依次执行指定函数,每一次的运算结果都会进入一个累积变量。
arr.reduce(function (preValue,curValue,index,array) {
return preValue + curValue;
});

那么,我们用ramdajs中的reduce来处理一下

1
2
3
4
5
var add = function (a, b) {
return a + b;
};
R.reduce(add, 0)(R.range(0,11)) // 55
//R.range(0,11) 输出为 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

FP特点三 柯里化

柯里化(又称部分求值),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数(多参转为单参函数)

举栗子
1
2
3
4
5
6
var addFourNumbers = (a, b, c, d) =>
a + b + c + d;
var curriedAddFourNumbers = R.curry(addFourNumbers);//通过R.curry对普通函数进行柯里化
var f = curriedAddFourNumbers(1, 2);//先传入部分参数,直到参数传齐才求值,惰性求值
var g = f(3);
g(4) // 10

map我们经常使用,但是,我们有什么理由用函数式里的map呢
首先,看一下map的基本使用

1
2
3
4
5
var double = x => x * 2;
var oneParam = R.map(double);
oneParam([2,4,5,6,7]) //[4, 8, 10, 12, 14]
oneParam({x: 1, y: 2, z: 3}) //{x: 2, y: 4, z: 6}
//和js原型上的map的区别是 数据的接收方式不同,与R.map是柯里化后的。
1
2
3
4
5
6
7
8
9
var users = [
{name: 'chet', age:25},
{name:'joe', age:24}
];
var a =R.pipe(
R.sortBy(R.prop('age')), //根据age的属性值来排序,输出排序后的数组对象 输出[{name:'joe', age:24},{name: 'chet', age:25}]
R.map(R.prop('name')), // 输入 [{name:'joe', age:24},{name: 'chet', age:25}] 输出["joe", "chet"]
R.join(',') ); //相当于 R.join(',')(["joe", "chet"])
a(users) // 最终得到 "joe,chet"

FP特点四 高阶函数

函数可以作为参数被传递;函数可以作为返回值输出。
将复用的粒度降低到函数级别,相对于面向对象语言,复用的粒度更低。

举栗子
1
2
3
4
5
compose (将多个函数合并成一个函数,从右到左执行)
R.compose(Math.abs, R.add(1), R.multiply(2))(-4) // 7
//R.multiply(2) 输出-8
//R.add(1) 输入-8, 输出-7
//Math.abs 输入-7, 输出7
1
2
3
4
5
6
7
8
pipe(将多个函数合并成一个函数,从左到右执行)
var negative = x => -1 * x;
var increaseOne = x => x + 1;
var f = R.pipe(Math.pow, negative, increaseOne);
f(3, 4) // -80 => -(3^4) + 1
// Math.pow 输入 3和4,输出 81
// negative 输入 81, 输出 -81
// increaseOne 输入 -81, 输出 -80

函数式库在mvvm框架中的使用

####只要是处理数据,我们随时可以使用ramdajs这样的函数式库来处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
created(){
indexApi.getFloorsData((res) => {
let data = (typeof(res) === 'string' ? JSON.parse(res) : res);
if(data.code == 200) {
this.floorData = data;
let errorImg = commonImg.state.placeholderImgFifth;
let noObj = (item) => R.type(item.imgurl) != "Object";
let addProp = (item) => R.merge(item)({'imgurl': {src: item.imgurl,error: errorImg,loading: errorImg}});
let addImgProp = R.pipe(R.filter(noObj),R.map(addProp));
let baseItems = R.pipe(R.take(10),R.pluck('baseItems'))(this.floorData.floorMenuItems);//所有楼层数据
let baseItemsAddImgList = R.map(addImgProp)(baseItems);//增加默认图对象
let hasProdAndSkuNo = (item) => R.has('productId')(item) && R.has('skuNo')(item);
var prodIdSkuIdList = (item) => R.assoc("prodIdSkuId",R.prop('productId')(item) + '-' + R.prop('skuNo')(item))(item);
let mapPlusIds = R.pipe(R.filter(hasProdAndSkuNo),R.map(prodIdSkuIdList));

var baseItemsImgsAndproIdAndSkuNoList = R.map(mapPlusIds)(baseItemsAddImgList);//写入属性prodIdSkuId到baseItems中

var proIdAndSkuNoListList = R.pipe(R.map(R.pluck('prodIdSkuId')),R.join(','))(baseItemsImgsAndproIdAndSkuNoList);//得到productId-skuNo串

this.floorData.floorMenuItems =R.zipWith(R.assoc('baseItems'),baseItemsImgsAndproIdAndSkuNoList,this.floorData.floorMenuItems);//数据组合
}
indexApi.getPrice(proIdAndSkuNoListList, (priceRes) => {
let priceData = typeof(priceRes) === 'string' ? JSON.parse(priceRes) : priceRes;
if(R.prop('code')(priceData.result) == 200) {
R.flatten(baseItemsImgsAndproIdAndSkuNoList).map(function(v,index){;
Object.keys(priceData.data).map((pidSid) => {
if(R.equals(pidSid,v.prodIdSkuId)){
this.$set(v,'newPrice',priceData.data[pidSid]['price'].pcPrice);
}
});
});
}
});
}, (error) => {
console.log(error);
});
}

四,函数式编程四特征总结

  1. 纯函数 也称幂等函数 (纯函数,没有“副作用”)
  2. 不可变 (使代码更简单和安全)
  3. 柯里化 (多参函数转为可接收单参数函数)
  4. 高阶函数 (使函数的复用粒度更低)

通过 柯里化 , 高阶函数 可以得到接近自然语言的表达

当下流行的vue,react也都提供了函数式组件,使用函数式组件最大的好处就是它能够将容器型和展示型组件明确区分开来,避免产生大型以及杂乱的组件。没有 this 关键字也就意味着没有快捷方式在整个应用中随机地展开状态。

五,常见的函数式编程语言

###比较纯粹的函数式编程语言

比较纯粹的函数式编程语言 支持函数式的编程语言
Clojure C++
Erlang Java
Haskell javascript
Lisp php
Perl python
Schema ruby
Scala
elm

五,参考资料

  1. https://baike.baidu.com/item/%CE%BB%E6%BC%94%E7%AE%97/8019133?fr=aladdin
  2. https://ramdajs.com/
  3. https://baike.baidu.com/item/%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B/4035031?fr=aladdin
  4. 文中的代码片段可以直接引用ramdajs来运行。

最后,感谢您的耐心阅读,欢迎沟通,如有问题,欢迎指正。