编程泛式
编程泛式是计算机典范的模式和方法,常见的包括了过程式编程、面向对象编程、函数式编程、指令式编程等
那么,为什么要学编程范式呢?或者说编程范式它的意义在哪? 编程范式的意义在于它提供了模块化代码的各种思想和方法,可以更好的模块化
如果说,面向对象编程是以 对象 作为模块的单元,那么函数式编程则是以 函数 作为模块的单元。
下面就开始学习函数式编程吧
函数
学习函数式编程,肯定离不开函数。这个函数,指的是我们数学中的函数
数学中的函数
函数的在数学上的定义如下
设 A 和 B 是两个非空集合,如果按照某种对应关系
对于集合 A 中的任何一个元素 a,在集合 B 中都存在唯一的一个元素 b 与之对应,那么,这样的对应叫做 集合 A 到 集合 B 的 映射,记作
其中,b 称为 a 在 映射 f 下的象,记作
函数的三要素:定义域A,值域B,映射关系 ,即我们写 JS 函数时的 输入,输出,函数
纯函数
纯函数的概念:给定相同的输入,必定有相同输出的函数。
正如我们数学中函数的特点:一个 x 只对应一个 y
纯函数的好处
- 可预测 / 可测试
由于是纯函数,根据输入就可以知道输出,利用这个特性,我们就可以很容易的去断言输出,也就更容易测试
- 可缓存
由于给定输入,输出总是一定的。那么我们就可以将函数结果缓存起来,下次给定相同的输入,我们可以直接返回缓存的结果,而不用重新执行一次函数
// 例一:感受一下可缓存的意思/*** 模仿lodash的memorize,是一个纯函数* 缓存一个函数的返回值,如果再次拿这个值,直接从缓存拿** @param {Function} func 需要缓存化的函数* @param {Function} resolver 这个函数的返回值作为缓存的 key*/function memorize(func, resolver) {const memory = function (key) {const cache = memory.cache;const address = '' + (resolver ? resolver.apply(this, arguments) : key);if (Object.getOwnPropertyNames(cache).indexOf(address) === -1) {cache[address] = func.apply(this, arguments);}return cache[address];}memory.cache = {}return memory}const sin = memorize(x => Math.sin(x)); // sin 也是一个纯函数sin(1);sin(1); // 第二次获取是缓存的值let count = 0;const plusFour = memorize(n => {count++;return n * 4;})plusFour(2); // 8console.log(count); // 1plusFour(2); // 8console.log(count); // 1plusFour(3); // 12console.log(count); // 2plusFour.cache = {}; // 清空缓存
函数式编程
高阶函数
高阶函数要么是以函数作为参数,要么以函数作为返回值,要么兼而有之
在数学中,如果x = g(m),y = f(x),那么y = f(g(m))
// 例一:简单的例子// 常见写法function add(x) {return y => x + y;}// 箭头函数写法const add = x => y => x + y;const addOne = add(1);addOne(2); // 3
JS 中有自带的高阶函数,也许你日用而不知,如 map 、sort 、reduce、filter等,以及 JS 的事件也是高阶函数
// 例二:实现 map 、reduce 理解它们为什么是高阶函数,其他类推Array.prototype.map = function (callback) {let res = [];for (let i = 0, len = this.length; i < len; i++) {const item = callback.call(this, this[i], i, this);res.push(item);}return res;}Array.prototype.reduce = function (callback) {const len = this.length;let res = null;if (len < 2) return new Error("Array's length is less than 2");for (let i = 1; i < len; i++) {const preVal = res || this[i - 1];const item = callback.call(this, preVal, this[i], i, this);res = item;}return res;}
函数柯里化
柯里化:把接受多个参数的函数 变成 接受一个单一参数的函数, 并且返回接受余下的参数且返回结果的新函数的操作
用数学公式表示函数柯里化
柯里化后的函数:y = f(a,b,c,d) = f(a,b,c,d) = f(a,b,c)(d) = f(a,b)(c,d) = f()(a,b,c,d)
// 例一:简单例子理解柯里化// ES5var add = function(x) {return function(y) {return x + y;}}// ES6const add = x => y => x + y;add(1)(2);
// 例二:实现一个函数,可以将所有函数柯里化const createCurry = function (fn, ...firstArgs) {return function (...args) {// 首次柯里化时,若未提供firstArgs,则不拼接进argsif (firstArgs.length) args = firstArgs.concat(args);// 递归调用,若 args 参数长度不满足函数 fn 的参数要求,则将参数传入,并柯里化并返回if (args.length < fn.length) return createCurry(fn, ...args);// 递归出口,执行函数return fn.apply(null, args)}}const add = (x, y, z) => x + y + z;const addOneAndFive = createCurry(add, 1, 5);const res = addOneAndFive(1)console.log(res);
函数组合
函数组合:将函数串联起来执行,将多个函数组合起来,一个函数的输出结果是另一个函数的输入参数,连续调用各个函数后,返回结果的操作
数学公式表示组合
假设本来函数是这样的:y = f(g(k(p(x)))),有很多个嵌套函数
用函数组合的方式,将嵌套函数解耦:y = compose(f, g, k, p)(x),其中compose就是将函数组合起来的操作
// 例一:实现一个函数,可以将所有函数进行组合const compose = function (...fnArr) {// 如果没有传参,则返回一个函数if (!fnArr.length) return arg => arg;if (fnArr.length === 1) return fnArr[0];return fnArr.reduce((preFn, curFn) => (...rest) => curFn(preFn(...rest)))}const powSelf = (n) => n * n;const addSelf = (n) => n + n;const imSubSelf = (n) => n - n;const f = compose(imSubSelf, addSelf, powSelf);const res = f(2); // res = imSubSelf(addSelf(powSelf(2))) = 0
Pointfree
首先先了解程序的本质:
输入 (x) —> 运算 (f) —> 输出 (y)假设,运算过程有很多函数f1、f2、f3、f4、f5...,这些函数的参数我们是不知道的,可以自定义的。但一旦参数确定,运算结果我们是知道的
现在我们来说说,什么是 Pointfree 风格?
我们完全可以把数据处理的过程,定义成一种与参数无关的合成运算。不需要用到代表数据的那个参数,只要把一些简单的运算步骤合成在一起即可
这就叫做 Pointfree:不使用所要处理的值,只合成运算过程。中文可以译作”无值”风格
示例一
[{"user": "lucifer","posts": [{ "title": "fun fun function", "contents": "..." },{ "title": "time slice", "contents": "..." }]}, {"user": "lucifer","posts": [{ "title": "babel", "contents": "..." },{ "title": "webpack", "contents": "..." }]}, {"user": "karl","posts": [{ "title": "ramda", "contents": "..." },{ "title": "lodash", "contents": "..." }]}]
现在用 Pointfree 的编程风格,把 lucifer 的 所有 title 打印出来
// data 代表上述的 数组数据// 定义处理函数const promise = (data) => Promise.resolve(data);const filter = createCurry((fn, data) => data.filter(fn)); // function first, data lastconst getProp = createCurry((prop, data) => data[prop]);const equals = createCurry((a, b) => a === b);const chain = createCurry((fn, data) => {let res = []for (let i in data) {res = res.concat(fn(data[i]));}return res;});const map = createCurry((fn, data) => data.map(fn))// 开始处理数据promise(data).then(res => filter(compose(getProp('user'), equals('lucifer')))(res)).then(res => chain(getProp('posts'))(res)).then(res => map(getProp('title'))(res)).catch(err => console.log(err))
从上面,我们可以看到,调用 equals、getProp、chain、map 的时候,我们是传入所需要的参数,作为提取数据的依据,如果数据源改变了,我们只需要更改参数,而不需要更改定义的处理函数
// 获取 karl 的 contentpromise(data).then(filter(compose(getProp('user'), equals('karl')))).then(chain(getProp('posts'))).then(map(getProp('contents'))).catch(err => console.log(err))
📕 理解 filter(compose(equals('karl'), getProp('user')))
把 equals('karl') 看作 f,把 getProp('user') 看作 g
那么,compose(equals('karl'), getProp('user')) 本质就是 f(g(x))
