正在学习redux的源码,感觉redux的实现很多地方都是基于函数式编程,函数式编程思想好像也在前端的很多库中都有广泛使用。为了更好的理解redux的源码,加深实现的记忆,也为了帮助日后学习其他库,现在插个空儿,入个函数式编程的门儿。

学习材料是列在最后的《JS函数式编程指南》的中文译本。

笔记

第二章 一等公民的函数

看过这一章,有两点需要在日后的开发中注意:

  1. 使用函数无论在赋值给变量还是作为参数传入函数,都要注意函数是一等公民
  2. 变量、函数命名时要更具有通用性

关于上述第一点的理解,若日后不记得,可参考这里的例子


第三章 纯函数

这一章主要讲了函数式编程中的重要概念,纯函数,以及纯函数的优点。

  1. 纯函数的定义

纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。

  1. 作用

作用,我们可以理解为一切除结果计算之外发生的事情。

  1. 副作用

副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。

副作用可能包含但不限于:

  • 更改文件系统
  • 往数据库插入记录
  • 发送一个http请求
  • 可变数据
  • 打印/log
  • 获取用户输入
  • DOM查询
  • 访问系统状态
  • 。。。

概括来讲,只要是跟函数外部环境发生的交互就都是副作用。


第四章 函数柯里化

  1. curry的概念

只传给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

curry的用途是参数复用,降低通用性,提高适用性。

文中给出的例子可以进一步参考这里:https://github.com/lodash/lodash/wiki/FP-Guide#capped-iteratee-arguments

文中给出了这样一段:

只传给函数一部分参数通常也叫做局部调用(partial application),能够大量减少样板文件代码(boilerplate code)。

未能体会,留下作为未解之谜…

  1. curry函数的实现

实现代码来自参考资料2第二版(使用占位符的第三版判断条件较多,过于复杂)。

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
89
90
// 实现方式1

// sub_curry的作用就是用一个新的函数包裹原函数,然后给原函数传入之前的参数
function sub_curry(fn) {
const args = [].slice.call(arguments, 1);
return function() {
// 此处实际已经执行
return fn.apply(this, args.concat([].slice.call(arguments)));
};
}

function curry(fn, length) {
length = length || fn.length;
return function() {
if (arguments.length < length) {
const combined = [fn].concat(Array.prototype.slice.call(arguments));
// 注意:此处sub_curry.apply(this.combined)是执行了
// 返回一个包裹了原函数和参数的新函数
// 这个新函数用于获取后续参数
return curry(sub_curry.apply(this, combined), length - arguments.length);
} else {
return fn.apply(this, aruments);
}
};
}

// 执行步骤分析
// 这里fn0是一个纯函数
// curry函数也是一个纯函数
// 下述步骤分析过程
// 使用了纯函数中的`等式推导`
// 也就是`引用透明`
const fn0 = function(a, b, c, d) {
return [a, b, c, d];
}
const fn1 = curry(fn0);
fn1('a', 'b')('c')('d');

// 当执行fn1('a', 'b')时

// fn1('a', 'b')相当于
curry(fn0)('a', 'b');
// 相当于
curry(sub_curry(fn0, 'a', 'b'));
// 相当于
curry(function(..) {return fn0('a', 'b', ...)});

// 当执行fn1('a', 'b')('c')时

// fn1('a', 'b')('c')返回
curry(sub_curry(function(..) {return fn0('a', 'b', ...)}, 'c'));
// 相当于
curry(function(...) {return fn0('a', 'b', 'c', ...)});

// 当执行fn1('a', ''b)('c')('d')时,此时arguments.length < length 为false,执行fn(arguments)

// 相当于
(function(...) {
return fn0('a', 'b', 'c', ...);
})('d)

// 相当于
fn0('a', 'b', 'c', 'd');

// 函数执行完毕

// =================================

// 实现方式2
// 这种方式更直观
// 每次只组合参数
// 最后一次性传入原函数中
// 而实现方式1是每次都会向原函数中传入参数但延迟执行
function curry(fn, args) {
const length = fn.length;
args = args || [];
return function() {
const _args = args.slice();
let arg, i;
for (i = 0; i < arguments.length; i++) {
arg = arguments[i];
_args.push(arg);
}
if (_args.length < length) {
return curry.call(this, fn, _args);
} else {
return fn.apply(this, _args);
}
}
}

实现的本质是通过高阶函数,利用闭包递归保存每次传入的参数同时延迟业务函数的执行,只有当参数数量等于业务函数参数数量时,才执行业务函数计算结果。


第五章 组合

  1. redux中compose的实现
1
2
3
4
5
6
7
8
9
10
11
function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}

if (funcs.length === 1) {
return funcs[0]
}

return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
  1. 结合律

所有的组合都满足结合律。结合律的一大好处是任何一个函数分组都可以被拆开来,然后再以它们自己的组合方式打包在一起。通过这种方式能够构建出很多有用的组合功能。

  1. pointfree模式

函数无须提及将要操作的数据是什么样的。一等公民的函数、柯里化(curry)以及组合协作起来非常有助于实现这种模式。

1
const associative = compose(f, compose(g, h)) == compose(compose(f, g), h);

第六章 示例应用

  1. map 的组合律
1
var law = compose(map(f), map(g)) == map(compose(f, g));
  1. 示例核心代码
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
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 (_, $) {
const trace = _.curry(function(tag, x) {
console.log(tag, x);
return x;
});

const Impure = {
getJSON: _.curry(function(callback, url) {
$.getJSON(url, callback);
}),
setHtml: _.curry(function(sel, html) {
$(sel).html(html);
}),
}

const url = function(term) {
return 'https://api.flickr.com/services/feeds/photos_public.gne?tags=' + term + '&format=json&jsoncallback=?';
}

const img = function (url) {
return $('<img />', { src: url});
};

// 第一版代码
const mediaUrl = _.compose(_.prop('m'), _.prop('media'));
const srcs = _.compose(_.map(mediaUrl), _.prop('items'));
const images = _.compose(_.map(img), srcs);
const renderImages = _.compose(Impure.setHtml('body'), images);
const app = _.compose(Impure.getJSON(renderImages), url);

// 利用等式推倒以及纯函数的特性
// 重构
var mediaUrl = _.compose(_.prop('m'), _.prop('media'));

var mediaToImg = _.compose(img, mediaUrl);

var images = _.compose(_.map(mediaToImg), _.prop('items'));


app('dog');
});

跟着写完这个部分的demo,最大的感受就是函数式编程的本质就是数学等式的变换。函数式的写法是写成声明式的而非命令式的。

第七章 类型系统

基本没有get到作者的点。。。


第八章 容器

functor:是实现了map函数并遵守一些特定规则的容器类型。

文章描述了三种functor用来解决不同的问题

  1. Maybe:用来处理空值
  2. Either:用来处理两个分支,这两个分支各代表一种状态,其和是所有的状态的集合
  3. IO:延迟非纯操作的执行,将其启动权利交由调用者,转化为纯操作
  4. Task:这里用到了task,fork,redux-saga中也有相同的概念

第九章 Monad

join:合并容器

chain:链式调用

of:向容器中加入值

map:在不脱离容器的情况下使用态射改变值到新的范畴中

Monad是阻塞的,可以改变容器类型。

最后讲了他们之间的关系,同一律和结合律,同一律说实话没咋看明白。


第十章 Applicative functor

ap:就是这样一种函数,能够把一个 functor 的函数值应用到另一个 functor 的值上。

一个ap的实现:

1
2
3
Container.prototype.ap = function(other_container) {
return other_container.map(this.__value);
}

Applicative functor是非阻塞的,不会改变容器类型。

最后又讲了好几个定律:同一律、同态、互换以及组合。

由于函数式编程能严格遵循这些定律,推导出不同的代码形式,所以如果要深入的话,这些必须得好好学下。不过,目前只是入个门,这些待议。


参考资料

  1. https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/ (《JS函数式编程指南》)
  2. https://github.com/mqyqingfeng/Blog/issues/42 (关于柯里化的资料)
  3. https://github.com/lodash/lodash/wiki/FP-Guide#capped-iteratee-arguments (lodash中的fp关于《JS函数式编程指南》纯函数部分举的例子的描述)