编程泛式
编程泛式是计算机典范的模式和方法,常见的包括了过程式编程、面向对象编程、函数式编程、指令式编程等
那么,为什么要学编程范式呢?或者说编程范式它的意义在哪? 编程范式的意义在于它提供了模块化代码的各种思想和方法,可以更好的模块化
如果说,面向对象编程是以 对象 作为模块的单元,那么函数式编程则是以 函数 作为模块的单元。
下面就开始学习函数式编程吧
函数
学习函数式编程,肯定离不开函数。这个函数,指的是我们数学中的函数
数学中的函数
函数的在数学上的定义如下
设 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); // 8
console.log(count); // 1
plusFour(2); // 8
console.log(count); // 1
plusFour(3); // 12
console.log(count); // 2
plusFour.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)
// 例一:简单例子理解柯里化
// ES5
var add = function(x) {
return function(y) {
return x + y;
}
}
// ES6
const add = x => y => x + y;
add(1)(2);
// 例二:实现一个函数,可以将所有函数柯里化
const createCurry = function (fn, ...firstArgs) {
return function (...args) {
// 首次柯里化时,若未提供firstArgs,则不拼接进args
if (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 last
const 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 的 content
promise(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))