函数式编程

本文ppt请笑纳

函数式编程

维基百科对函数式编程的定义:函数式编程(英语:functional programming)或称函数程序设计,是一种编程典范,它将计算机运算视为数学上的函数计算,并且避免使用程序状态以及易变对象。这里的函数指的是数学上的函数,既自变量(数据)的映射。

命令式编程对比,函数式编程更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元(小函数)让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。

我的理解是,以纯函数( 避免使用程序状态以及异变对象)为单元,去抽象、拆分模块功能的编程思想。其实函数式编程我们平时不陌生,比如react, redux 都多多少少有一些相关。

所以我们得出几个函数式编程的要点:

  1. 避免使用程序状态以及异变对象,数据映射=> 纯函数
  2. 强调执行结果而非过程=> 声明式

纯函数

纯函数:输出结果只由输入决定,并且不产生副作用(effects)。

  1. 避免使用 this,window 等外部变量
  2. 没有产生副作用(Effects),既把Effects抽离出来

Effects有哪些呢?
比如 改动外部数据,发起ajax请求,改动dom等等。
写过dva的就知道,dva的model有一个effects,就是用来放这些副作用的代码。

我们把Effects分离出来之后,剩下的就是纯函数,纯函数可以很简单的进行单元测试,所以是不容易出bug的,因为往往有bug的都是不可预见的部分。

举个例子

1
2
3
const arr=['bilibili', 'hello', 'jsonz'];
arr.slice(1, 3); // 没副作用
arr.splice(1, 2); // 有副作用,修改到原来的arr数组 可能会造成意想不到的bug

虽然执行之后,都能得到 Hello Jsonz。 但是 splice 会改动到原来的数组,所以不算纯函数。

工作中其实很多工具类的function都是纯函数,redux的Reducer也是标准的纯函数。

redux reducers

1
2
3
4
5
6
function set(state, { payload }) {
return {
...state,
...payload,
}
}

纯函数的好处是,你不需要担心某个值在某处发送意想不到的改变导致出bug;纯函数里不产生副作用,所以也不会担心改动到其他地方,代码更健硕;可以更优雅的组合复用,方便移植到其他环境(web workers);方便的写单元测试。

声明式

在说声明式之前,要先普及一下与之对应的命令式(指令式)。
我们平时写的代码基本都是命令式,既我们通过一条一条的命令去执行一些操作,其中会涉及到很多细节的东西。
既关注执行过程,用各种控制语句if...else...for循环等。

命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。
声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。

SQL语句是典型声明式,SQL存储过程和存储函数则是命令式编程。

react里面的两种编程方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 函数式编程
const data= [{name: 'jsonz', age: 2}, { name: 'balala', age: 3}, { name: '小魔仙', age: 30}];

const dom = data.filter(item=> age<10)
.map(item=> <span>{item.name} - {item.age}<span>); // 这里后面的span是闭合的</span>



// 命令式编程
const data= [{name: 'jsonz', age: 2}, { name: 'balala', age: 3}, { name: '小魔仙', age: 30}];

let newData = [];
for (let i= 0; i< data.length; i++) {
if (data[i].age < 10) {
newData.push(data);
}
}

const dom2 = [];
for (let i= 0; i< newData.length; i++) {
dom2.push(`<span>${item.name}-${item.age}<span>`);
}

我们可以看出声明式的代码更加简洁清晰优雅。

声明式代码复用
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
// 双倍
function double(arr) {
const results = [];
for (let i = 0; i< arr.length; i++) {
results.push(arr[i] * 2);
}
return results;
}

// +1
function addOne(arr) {
const results = [];
for (let i = 0; i< arr.length; i++) {
results.push(arr[i] +1);
}
return results;
}

// 声明式: 去重复代码,简洁
function double(arr) {
return arr.map(item=> item*2);
}

function addOne(arr) {
return arr.map(item=> item+1);
}

// 声明式究极版: 借助 Ramda
const m= _.curry((fn, arr) => arr.map(item=> fn(item)));
const double = m(item=> item*2);
const addOne = m(item=> item+1);

这里我们可以看出一些函数式最简单的思路:
把循环和判断等控制语句尽可能换成筛选 filter映射 map化约 reduce

cookie获取
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

// 命令式
// 获取cookie
var _cookie = document.cookie;
// cookie 的形式是字符串 "key1=value1; key2=value2; "; 把他转变成数组形式 ["key1=value1", "key2=value2"]。 用 "; "作为分隔符

var _cookieArray = _cookie.split('; ');
// 循环把数组里面的 key1=value1变成对象obj的key和value 方便获取

var obj = {};
for (var i= 0; i< _cookieArray.length; i++) {

// 取出cookie数组里面的单个: key1=value1
var cookieItem = _cookieArray[i];

// 把这个字符串key1= value1 变换成数组 [key1, value1],用 "=" 做为分割符
var cookieItemArray = cookieItem.split('='); // [key1, value1]

// 把分割好的数组,赋值给obj
var key = cookieItemArray[0];
var value = cookieItemArray[1];
obj[key] = value;
}

// 函数式
document.cookie.split('; ')
.map(item=> {
const [key, value] = item.split('=');
return {key, value};
})
.reduce((acc, cur)=> {
const { key, value } = cur;
acc[key] = value;
return acc;
}, {});

综合 纯函数 + 声明式

该部分源代码可以看这个仓库 ( ☉_☉)≡☞o────★°仓库地址

求完美数
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
//  完全数(Perfect number),又称完美数或完备数,是一些特殊的自然数。 它所有的真因子(即除了自身以外的约数)的和(即因子函数),恰好等于它本身
// 比如 6 = 1 + 2 + 3 是完美数; 8 != 1 + 2 + 4 不是完美数
//
{
let num = 6;
function isPerfect() {
return aliquotSum() === num;
}

function isFactor(potential) {
return num % potential === 0;
}

function getFactors() {
const arr = [];

arr.push(1);
arr.push(num);

for (let i= 2; i< num; i++) {
if (isFactor(i)) arr.push(i);
}

return arr;
}


function aliquotSum() {
let sum = 0;
const factors = getFactors(num);
for (let i= 0; i< factors.length; i++) {
sum += factors[i];
}
sum -= num;
return sum;
}

const result = isPerfect();
console.log(result);
}


// 向函数式靠拢
// 1. 将 num 变为传参 而不是外部状态
// 2. 使得所有方法都变为纯函数
// 3. 真因子相加改为函数式写法
{
function isPerfect(num) {
return aliquotSum(num) === num;
}

function isFactor(num, potential) {
return num % potential === 0;
}

function getFactors(num) {
return Array.from(Array(num), (item, i)=> i)
.filter(v=> isFactor(num, v));
}


function aliquotSum(num) {
let sum = 0;
const factors = getFactors(num);
for (let i= 0; i< factors.length; i++) {
sum += factors[i];
}
return sum;
}

const result = isPerfect(6);
console.log(result);
}


// 究极版
{
const aliquotSum = num=> Array.from(Array(num), (item, i)=> i)
.filter(item=> num % item === 0)
.reduce((cur, next)=> cur += next);

function isPerfect(num) {
return aliquotSum(num) === num;
}

const result = isPerfect(6);
console.log(result);
}

上面都是比较小的工具demo,基本上不包含Effects,但是实际上项目充斥一堆业务逻辑(Effects),所以一般会借助一些工具/库来实现,比如 ramda.jsrx.js

rxJs 官网的描述是 Reactive Extensions Library for JavaScript简单来说RxJS就是辅助我们写出函数式响应式代码的一种工具。

我们现在来实现一个功能,页面上有一个按钮,点击之后输出计数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 普通版本
{
let count = 0;
const button = document.querySelector('#button');
button.addEventListener('click', ()=> {
count += 1; // 用到外部变量
console.log(`clicked!${count} times`);
});
}


// RxJs版本
{
const { fromEvent, operators: { scan, } } = rxjs;
const button = document.querySelector('#button');

fromEvent(button, 'click')
.pipe(
// scan 随着时间的推移进行归并。
scan((count)=> count+ 1, 0)
)
.subscribe(count=> console.log(`clicked! ${count} times`));
}

节流和累加点击坐标

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
{
let count = 0;
const button = document.querySelector('#button');
const rate = 1000;
let lastClick = Date.now() - rate;
button.addEventListener('click', (e)=> {
if (Date.now() - lastClick >= rate) {
count += e.clientX;
console.log(`clicked!${count} times`);
lastClick = Date.now();
}
});
}

{
const { fromEvent, operators: { scan, throttleTime, map } } = rxjs;
const button = document.querySelector('#button');

fromEvent(button, 'click')
.pipe(
throttleTime(1000),
map(e=> e.clientX),
scan((count, clientX)=> count+ clientX, 0)
)
.subscribe(count=> console.log(`clicked! ${count} times`));
}

接下来我们用Ramda.js来实现一个页面,获取某个关键字图片,并展示出来。Ramda主要帮忙实现一些 curry, compose 等功能

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
38
39
40
41
42
43
44
45
46
requirejs.config({
paths: {
ramda: 'https://cdnjs.cloudflare.com/ajax/libs/ramda/0.13.0/ramda.min',
jquery: 'https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min'
}
});

require([
'ramda',
'jquery'
],
function (_, $) {

// 我们把不纯的Effects抽出来
const Impure = {
getJSON: _.curry((callback, url)=> $.getJSON(url, callback)),

setHtml: _.curry((sel, html)=> $(sel).html(html)),
};

// 传入url 生成 img dom
const img = function(url) {
return $('<img />', { src: url });
};

// 获取传入参数的 media 属性,以及获取 media属性上的 m。 等价于 arg.media.m
const mediaUrl = _.compose(_.prop('m'), _.prop('media'));

// 获取传入参数的 item 属性,以及对每个item执行 mediaUrl 函数
const srcs = _.compose(_.map(mediaUrl), _.prop('items'));

// 把img和srcs函数整合起来
const images = _.compose(_.map(img), srcs);

const url = term=> `https://api.flickr.com/services/feeds/photos_public.gne
?tags=${term}&format=json&jsoncallback=?`;

// 把imges获取到的参数传给 Impure.setHtml('body');
const renderImages = _.compose(Impure.setHtml('body'), images);

// 把所有小函数整合成一个大的函数,我们只需要给这个app喂数据即可
const app = _.compose(Impure.getJSON(renderImages), url);

app('cat');

});

优化

1
2
3
4
5
6
7
8
9
10
11
// old
const mediaUrl = _.compose(_.prop('m'), _.prop('media'));

const srcs = _.compose(_.map(mediaUrl), _.prop('items'));

const images = _.compose(_.map(img), srcs);

// new
const mediaUrl = _.compose(_.prop('m'), _.prop('media'));

const images = _.compose(_.map(img), _.map(mediaUrl), _.prop('items'));

再优化

1
const images = _.compose(_.map(_.compose(img, mediaUrl)), _.prop('items'));

函数式和面向对象对比

面向对象编程思想,封装 继承 多态

把状态的改变封装起来,让代码更加清晰,通过类与继承去处理关联关系。但是会充满公用变量,thisbind等。

面向对象目前更加广泛,而且更容易被接受,写起来更容易,但可能会有更多冗余的代码,捆绑了很多状态,典型的问题是 你只需要一个香蕉,但是却得到一个拿着香蕉的大猩猩以及整个丛林。


函数式编程思想,尽量减少不确定因素,来让代码更加清晰健硕。

每个函数都不要去修改原有的数据,而是通过产生新的数据来做为运算结果,这也意味着函数式比较耗资源,所以在计算机石器时代并不流行

简单来说就是很多小的纯函数,一个小函数只做一些事情,然后用这些小函数来组织成更大的函数,函数的参数与返回值也大部分都是函数。
最后能得到一个超级牛逼的函数,我们只要把数据给他,就可以了。

函数式简洁,清晰可复用性高,出错几率更小。但是一开始思维很难从面向对象或命令式转换过来,往往也需要借助一些工具库来编写。

结语

这里只提了函数式编程的一些思想,但是真正函数式开发还包含很多其他要学的。比如我们用到的柯里化curry还有代码组合compose等,光RxJs提供的api就有179个之多…

再者不知道是不是对函数式的理解不够透彻,有试过某个需求用函数式的思想去写,但是有点好像硬掰的样子,写的有点不伦不类….所以现在只是吸收一些函数式的思想(比如纯函数减少不确定性),但是平时工作不会很刻意全部都用函数式去写。只是当成一个可以扩展的知识面,后面如果函数式真的在前端流行起来,再上手也不迟。

顺路感慨JS的灵活性,既有ES6面向对象的特性class也有ES5的 filtermapreduce,以及ES.Next的装饰器(Decorator)等函数式编程的特性。

最后再给出参考的以及学习过程看到的一些不错的书籍和文章:

JS 函数式编程指南
深入浅出Rxjs
函数式编程思维
命令式、声明式、面向对象、函数式、控制反转之华山论剑(上)
RxJs 响应式代码库
Ramda 函数式工具库
Haskell 纯函数式编程语言
什么是函数式编程思维
函数式编程术语解析