正在学习redux的源码,感觉redux的实现很多地方都是基于函数式编程,函数式编程思想好像也在前端的很多库中都有广泛使用。为了更好的理解redux的源码,加深实现的记忆,也为了帮助日后学习其他库,现在插个空儿,入个函数式编程的门儿。
学习材料是列在最后的《JS函数式编程指南》的中文译本。
笔记
第二章 一等公民的函数
看过这一章,有两点需要在日后的开发中注意:
- 使用函数无论在赋值给变量还是作为参数传入函数,都要注意函数是一等公民
- 变量、函数命名时要更具有通用性
关于上述第一点的理解,若日后不记得,可参考这里的例子。
第三章 纯函数
这一章主要讲了函数式编程中的重要概念,纯函数,以及纯函数的优点。
- 纯函数的定义
纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。
- 作用
作用,我们可以理解为一切除结果计算之外发生的事情。
- 副作用
副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。
副作用可能包含但不限于:
- 更改文件系统
- 往数据库插入记录
- 发送一个http请求
- 可变数据
- 打印/log
- 获取用户输入
- DOM查询
- 访问系统状态
- 。。。
概括来讲,只要是跟函数外部环境发生的交互就都是副作用。
第四章 函数柯里化
- curry的概念
只传给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
curry的用途是参数复用,降低通用性,提高适用性。
文中给出的例子可以进一步参考这里:https://github.com/lodash/lodash/wiki/FP-Guide#capped-iteratee-arguments
文中给出了这样一段:
只传给函数一部分参数通常也叫做局部调用(partial application),能够大量减少样板文件代码(boilerplate code)。
未能体会,留下作为未解之谜…
- 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
|
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)); return curry(sub_curry.apply(this, combined), length - arguments.length); } else { return fn.apply(this, aruments); } }; }
const fn0 = function(a, b, c, d) { return [a, b, c, d]; } const fn1 = curry(fn0); fn1('a', 'b')('c')('d');
curry(fn0)('a', 'b');
curry(sub_curry(fn0, 'a', 'b'));
curry(function(..) {return fn0('a', 'b', ...)});
curry(sub_curry(function(..) {return fn0('a', 'b', ...)}, 'c'));
curry(function(...) {return fn0('a', 'b', 'c', ...)});
(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); } } }
|
实现的本质是通过高阶函数,利用闭包递归保存每次传入的参数同时延迟业务函数的执行,只有当参数数量等于业务函数参数数量时,才执行业务函数计算结果。
第五章 组合
- 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))) }
|
- 结合律
所有的组合都满足结合律。结合律的一大好处是任何一个函数分组都可以被拆开来,然后再以它们自己的组合方式打包在一起。通过这种方式能够构建出很多有用的组合功能。
- pointfree模式
函数无须提及将要操作的数据是什么样的。一等公民的函数、柯里化(curry)以及组合协作起来非常有助于实现这种模式。
1
| const associative = compose(f, compose(g, h)) == compose(compose(f, g), h);
|
第六章 示例应用
- map 的组合律
1
| var law = compose(map(f), map(g)) == map(compose(f, g));
|
- 示例核心代码
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用来解决不同的问题
- Maybe:用来处理空值
- Either:用来处理两个分支,这两个分支各代表一种状态,其和是所有的状态的集合
- IO:延迟非纯操作的执行,将其启动权利交由调用者,转化为纯操作
- 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是非阻塞的,不会改变容器类型。
最后又讲了好几个定律:同一律、同态、互换以及组合。
由于函数式编程能严格遵循这些定律,推导出不同的代码形式,所以如果要深入的话,这些必须得好好学下。不过,目前只是入个门,这些待议。
参考资料
- https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/ (《JS函数式编程指南》)
- https://github.com/mqyqingfeng/Blog/issues/42 (关于柯里化的资料)
- https://github.com/lodash/lodash/wiki/FP-Guide#capped-iteratee-arguments (lodash中的fp关于《JS函数式编程指南》纯函数部分举的例子的描述)