编程泛式

编程泛式是计算机典范的模式和方法,常见的包括了过程式编程、面向对象编程、函数式编程、指令式编程等

那么,为什么要学编程范式呢?或者说编程范式它的意义在哪? 编程范式的意义在于它提供了模块化代码的各种思想和方法,可以更好的模块化

如果说,面向对象编程是以 对象 作为模块的单元,那么函数式编程则是以 函数 作为模块的单元。
下面就开始学习函数式编程吧

函数

学习函数式编程,肯定离不开函数。这个函数,指的是我们数学中的函数 函数式编程之基础篇 - 图1

数学中的函数

函数的在数学上的定义如下

设 A 和 B 是两个非空集合,如果按照某种对应关系函数式编程之基础篇 - 图2 对于集合 A 中的任何一个元素 a,在集合 B 中都存在唯一的一个元素 b 与之对应,那么,这样的对应叫做 集合 A 到 集合 B 的 映射,记作函数式编程之基础篇 - 图3 其中,b 称为 a 在 映射 f 下的象,记作 函数式编程之基础篇 - 图4

函数的三要素:定义域A,值域B,映射关系 函数式编程之基础篇 - 图5 ,即我们写 JS 函数时的 输入,输出,函数

纯函数

纯函数的概念:给定相同的输入,必定有相同输出的函数。

正如我们数学中函数的特点:一个 x 只对应一个 y

纯函数的好处

  1. 可预测 / 可测试

由于是纯函数,根据输入就可以知道输出,利用这个特性,我们就可以很容易的去断言输出,也就更容易测试

  1. 可缓存

由于给定输入,输出总是一定的。那么我们就可以将函数结果缓存起来,下次给定相同的输入,我们可以直接返回缓存的结果,而不用重新执行一次函数

  1. // 例一:感受一下可缓存的意思
  2. /**
  3. * 模仿lodash的memorize,是一个纯函数
  4. * 缓存一个函数的返回值,如果再次拿这个值,直接从缓存拿
  5. *
  6. * @param {Function} func 需要缓存化的函数
  7. * @param {Function} resolver 这个函数的返回值作为缓存的 key
  8. */
  9. function memorize(func, resolver) {
  10. const memory = function (key) {
  11. const cache = memory.cache;
  12. const address = '' + (resolver ? resolver.apply(this, arguments) : key);
  13. if (Object.getOwnPropertyNames(cache).indexOf(address) === -1) {
  14. cache[address] = func.apply(this, arguments);
  15. }
  16. return cache[address];
  17. }
  18. memory.cache = {}
  19. return memory
  20. }
  21. const sin = memorize(x => Math.sin(x)); // sin 也是一个纯函数
  22. sin(1);
  23. sin(1); // 第二次获取是缓存的值
  24. let count = 0;
  25. const plusFour = memorize(n => {
  26. count++;
  27. return n * 4;
  28. })
  29. plusFour(2); // 8
  30. console.log(count); // 1
  31. plusFour(2); // 8
  32. console.log(count); // 1
  33. plusFour(3); // 12
  34. console.log(count); // 2
  35. plusFour.cache = {}; // 清空缓存

函数式编程

高阶函数

高阶函数要么是以函数作为参数,要么以函数作为返回值,要么兼而有之
在数学中,如果x = g(m)y = f(x),那么y = f(g(m))

  1. // 例一:简单的例子
  2. // 常见写法
  3. function add(x) {
  4. return y => x + y;
  5. }
  6. // 箭头函数写法
  7. const add = x => y => x + y;
  8. const addOne = add(1);
  9. addOne(2); // 3

JS 中有自带的高阶函数,也许你日用而不知,如 map 、sort 、reduce、filter等,以及 JS 的事件也是高阶函数

  1. // 例二:实现 map 、reduce 理解它们为什么是高阶函数,其他类推
  2. Array.prototype.map = function (callback) {
  3. let res = [];
  4. for (let i = 0, len = this.length; i < len; i++) {
  5. const item = callback.call(this, this[i], i, this);
  6. res.push(item);
  7. }
  8. return res;
  9. }
  10. Array.prototype.reduce = function (callback) {
  11. const len = this.length;
  12. let res = null;
  13. if (len < 2) return new Error("Array's length is less than 2");
  14. for (let i = 1; i < len; i++) {
  15. const preVal = res || this[i - 1];
  16. const item = callback.call(this, preVal, this[i], i, this);
  17. res = item;
  18. }
  19. return res;
  20. }

函数柯里化

柯里化:把接受多个参数的函数 变成 接受一个单一参数的函数, 并且返回接受余下的参数且返回结果的新函数的操作

用数学公式表示函数柯里化
柯里化后的函数: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)

  1. // 例一:简单例子理解柯里化
  2. // ES5
  3. var add = function(x) {
  4. return function(y) {
  5. return x + y;
  6. }
  7. }
  8. // ES6
  9. const add = x => y => x + y;
  10. add(1)(2);
  1. // 例二:实现一个函数,可以将所有函数柯里化
  2. const createCurry = function (fn, ...firstArgs) {
  3. return function (...args) {
  4. // 首次柯里化时,若未提供firstArgs,则不拼接进args
  5. if (firstArgs.length) args = firstArgs.concat(args);
  6. // 递归调用,若 args 参数长度不满足函数 fn 的参数要求,则将参数传入,并柯里化并返回
  7. if (args.length < fn.length) return createCurry(fn, ...args);
  8. // 递归出口,执行函数
  9. return fn.apply(null, args)
  10. }
  11. }
  12. const add = (x, y, z) => x + y + z;
  13. const addOneAndFive = createCurry(add, 1, 5);
  14. const res = addOneAndFive(1)
  15. console.log(res);

函数组合

函数组合:将函数串联起来执行,将多个函数组合起来,一个函数的输出结果是另一个函数的输入参数,连续调用各个函数后,返回结果的操作

数学公式表示组合
假设本来函数是这样的:y = f(g(k(p(x)))),有很多个嵌套函数
用函数组合的方式,将嵌套函数解耦:y = compose(f, g, k, p)(x),其中compose就是将函数组合起来的操作

  1. // 例一:实现一个函数,可以将所有函数进行组合
  2. const compose = function (...fnArr) {
  3. // 如果没有传参,则返回一个函数
  4. if (!fnArr.length) return arg => arg;
  5. if (fnArr.length === 1) return fnArr[0];
  6. return fnArr.reduce((preFn, curFn) => (...rest) => curFn(preFn(...rest)))
  7. }
  8. const powSelf = (n) => n * n;
  9. const addSelf = (n) => n + n;
  10. const imSubSelf = (n) => n - n;
  11. const f = compose(imSubSelf, addSelf, powSelf);
  12. const res = f(2); // res = imSubSelf(addSelf(powSelf(2))) = 0

Pointfree

首先先了解程序的本质:输入 (x) —> 运算 (f) —> 输出 (y) 假设,运算过程有很多函数f1、f2、f3、f4、f5...,这些函数的参数我们是不知道的,可以自定义的。但一旦参数确定,运算结果我们是知道的

现在我们来说说,什么是 Pointfree 风格?
我们完全可以把数据处理的过程,定义成一种与参数无关的合成运算。不需要用到代表数据的那个参数,只要把一些简单的运算步骤合成在一起即可
这就叫做 Pointfree:不使用所要处理的值,只合成运算过程。中文可以译作”无值”风格

示例一

  1. [{
  2. "user": "lucifer",
  3. "posts": [
  4. { "title": "fun fun function", "contents": "..." },
  5. { "title": "time slice", "contents": "..." }
  6. ]
  7. }, {
  8. "user": "lucifer",
  9. "posts": [
  10. { "title": "babel", "contents": "..." },
  11. { "title": "webpack", "contents": "..." }
  12. ]
  13. }, {
  14. "user": "karl",
  15. "posts": [
  16. { "title": "ramda", "contents": "..." },
  17. { "title": "lodash", "contents": "..." }
  18. ]
  19. }]

现在用 Pointfree 的编程风格,把 lucifer 的 所有 title 打印出来

  1. // data 代表上述的 数组数据
  2. // 定义处理函数
  3. const promise = (data) => Promise.resolve(data);
  4. const filter = createCurry((fn, data) => data.filter(fn)); // function first, data last
  5. const getProp = createCurry((prop, data) => data[prop]);
  6. const equals = createCurry((a, b) => a === b);
  7. const chain = createCurry((fn, data) => {
  8. let res = []
  9. for (let i in data) {
  10. res = res.concat(fn(data[i]));
  11. }
  12. return res;
  13. });
  14. const map = createCurry((fn, data) => data.map(fn))
  15. // 开始处理数据
  16. promise(data)
  17. .then(res => filter(compose(getProp('user'), equals('lucifer')))(res))
  18. .then(res => chain(getProp('posts'))(res))
  19. .then(res => map(getProp('title'))(res))
  20. .catch(err => console.log(err))

从上面,我们可以看到,调用 equals、getProp、chain、map 的时候,我们是传入所需要的参数,作为提取数据的依据,如果数据源改变了,我们只需要更改参数,而不需要更改定义的处理函数

  1. // 获取 karl 的 content
  2. promise(data)
  3. .then(filter(compose(getProp('user'), equals('karl'))))
  4. .then(chain(getProp('posts')))
  5. .then(map(getProp('contents')))
  6. .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))

参考资料

函数式编程入门
Pointfree编程风格 —— 阮一峰
JavaScript函数式编程