纯函数

相同的输入永远会得到形同的输出。而且没有任何副作用,也不依赖外部环境的状态。
例如:Array.slice() 就是一个纯函数,他没有任何副作用,相同的输入会有相同的输出。

通过代码来看:

  1. var arr = [1, 2, 3, 4, 5];
  2. arr.slice(0, 3); // => [1, 2, 3, 4]
  3. arr.slice(0, 3); // => [1, 2, 3, 4]
  4. arr.slice(0, 3); // => [1, 2, 3, 4]
  5. arr.slice(0, 3); // => [1, 2, 3, 4]

首先它每次对 arr 操作的时候不会改变原先的 arr,不修改状态。它也不依赖于外部的任何东西。那么它也没有副作用,最后它每次相同的输入得到的值都是一样的。这就是一个纯函数。

优点

  1. 降低系统的复杂度
  2. 缓存
  3. 预先求值的替代性

    偏应用函数

    带一个函数参数和该函数的部分参数。

通过代码来看:

  1. const partial = (f, ...args) => (...moreArgs) => f(...args, ...moreArgs);

接收一个函数 f 和 其他参数,返回一个函数,可以接收更多参数。然后再返回 f 函数的执行结果。把开始的参数和接收到的更多参数,都放在 f 里面执行。

柯里化

柯里化就是偏应用函数的的具体实现。它是把一个多参数的偏应用函数转为一个嵌套一元函数的过程。传递一个参数返回一个函数去处理剩下的参数。

通过代码来看:

  1. const checkage = min => (age => age > min);
  2. const checkage18 = checkage(18);
  3. checkage18(20);
  4. // 也可以这样写
  5. checkage(18)(20);

首先传递一个参数 min —>18 然后返回一个函数, 返回的参数是可以拿到父层的作用域中的 min 的,然后这个返回的函数也接收一个参数 age,那么就可以做到每次只传递一个参数。

反柯里化

函数柯里化是固定部分参数,返回一个接受剩余参数的函数,也称为部分计算函数,目的是为了缩小使用范围,创建一个针对性更强的函数。
反柯里化,意义和用法跟柯里化正好相反,扩大使用范围,创建一个应用范围更广的函数。使本来只有特定对象只适用的方法,扩展到更多的对象。

通过代码来看:

  1. Function.prototype.unCurring = function(){
  2. var self = this;
  3. return function(){
  4. var obj = Array.prototype.shift.call(arguments);
  5. return self.apply(obj, arguments);
  6. }
  7. }
  8. var push = Array.prototype.push.unCurring(),
  9. obj = {};
  10. push(obj, "first", "two");
  11. console.log(obj);

反柯里化就是为了更多的去一次性接收数组。扩大使用范围。

函数的柯里化

  1. const curry = (fn, arr = []) =>
  2. (...args) =>
  3. (arg => (arg.length === fn.length) ? fn(arg)):
  4. curry(fn, arg)))([...arr, ...args]);
  5. let curryTest = curry((a, b, c, d) => a + b + c + d);
  6. curryTest(1, 2, 3)(4); // => 10
  7. curryTest(1, 2)(4)(3); // => 10
  8. curryTest(1, 2)(3, 4); // => 10

优点

柯里化是一种函数预加载的办法,通过传递觉少的参数,得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种对参数的缓存。是一中非常搞笑的编写函数的方法。

柯里化和偏应用的区别

柯里化的参数是从左向右的,如果使用 setTimeout 这种就得额外的封装。偏应用函数是从右向左的。

  1. const setTimeoutWraper = (timeout, fn) => {
  2. setTimeout(fn, timeout);
  3. }
  4. const delaytenMS = curry(setTimeoutWraper)(10);
  5. delayTenMS(() => console.log("do x Task"));
  6. delayTenMS(() => console.log("do y Task"));

函数组合

纯函数以及柯里化写着写着就容易出现嵌套特别深的代码: h( g( f(x) ) ),为了解决这种嵌套,就需要用到函数组合。

通过代码来看:

  1. const compose = (f, g) => (x => f(g(x)));
  2. const first = arr => arr[0];
  3. const reverse = arr => arr.reverse();
  4. const last= compose(first, reverse);
  5. last([1, 2, 3, 4, 5]);

其实就是把函数在内部执行一下。

函数式编程常用核心概念 - 图1

  1. compose(f, compose(g, h));
  2. compose(compose(f,g), h);
  3. compose(f, g, h)

函数组合会让内部的调用更加灵活,f 可以和 g 组合,f 也何以和 h 组合, h 和 g 也能够组合。

函数组合子

组合子(全称:组合子函数)又称之为装饰器函数,用于转换函数或数据,增强函数或数据行为的高阶函数
函数组合数据流是从右至左的,因为最右边的函数首先执行,讲数据传递给下一个函数以此类推。可以用另一种方式左边的先执行。可以实现pipe来实现。它和compose所做的一样,只不过交换了数据方向。
因此我们需要组合子管理程序的控制流。
命令式的代码能够使用 if..else 和 for 这样的过程控制,函数式则不能。所以需要函数组合子。组合子可以组合其他函数或者其他组合子,并作为控制逻辑单元的高阶函数,组合子通常不声明任何变量,也不包含任务业务逻辑,他们目的是在管理函数程序执行流程。并在链式调用中对中间结果进行操作。

常见组合子如下

辅助组合子

无为(nothing)、照旧(identity)、默许(defaultTo)、恒定(always)

函数组合子

收缩(gather)、展开(spread)、颠倒(reverse)、左偏(partial)、右偏(partialRight)、柯里化(curry)、弃离(tap)、交替(alt)、补救(tryCatch)、同时(seq)、聚集(converge)、映射(map)、分捡(useWith)、规约(reduce)、组合(compose)

谓语组合子

过滤(filter)、分组(group)、排序(sort)

其他

组合子变换juxt
分属于SKI组合子。(通过运算达到指定的目的)

Point Free

在实现函数式编程的时候不要有过多的中间变量。
通过代码来看:

  1. const f = str => str.toUpperCase().split('');
  2. // 正确书写格式
  3. const toUpperCase = word => word.toUpperCase();
  4. const split = x => (str => str.split(x))
  5. const f = (split(''), toYpperCase);
  6. f('abcd efgh');

声明式与命令式

命令式就是通过一条一条指令去执行一些动作。生命是就是通过表达式来声明干什么。

  1. // 命令式
  2. let ceo = [];
  3. for(let i; i< companies.length;i++){
  4. ceo.push(companies[i].value);
  5. }
  6. //声明式
  7. let ceo = companies.map(item => item.value);

优点

声明式的代码对于无副作用的纯函数,我们完全可以不考虑函数内部是如何实现的,专注于编写业务。优化代码时目光只需要集中在稳定兼顾的函数内部即可。

惰性链、惰性求值、惰性函数

_chain(data).map().reverse().value() 惰性链可以添加一个输入对象的状态,从而能够将这些输入转换为所需的输出操作连接在一起。在最后调用 value 之前并不会真的执行任何操作。这就是惰性链。
当输入很大但只有一个子集有效时,避免不必要的函数调用就是惰性求值。尽可能的推迟求值,直到以来的表达式被调用。
加入同一个函数被大量范围,并且这个函数内部又有许多判断来检测函数。这昂对于一个调用会浪费时间和浏览器资源,所以当第一次判断完成之后,直接把这个函数改写,不需要判断。

  1. //惰性求值
  2. const alt = _.curry((fn1, fn2, val) => fn1(val) || fn2(val));
  3. const showStudent = _.compose(函数体1, alt(xx1, xx2));
  4. showStudent({});
  5. let object = {a: 'xx', b: 2};
  6. let values= _.memoize(_.values);
  7. values(object);
  8. object.a = '京程一灯';
  9. console.log(values.cache.get(object));
  1. // 惰性连
  2. //_.chain可以推断可优化点,如合并执行后存储优化
  3. //合并函数执行 并压缩计算过程中的临时数据结构降低内存占用
  4. const trace = msg => console.log(msg);
  5. let square = x => Math.pow(x, 2);
  6. let isEven = x => x % 2 === 0;
  7. // 使用组合子跟踪
  8. square = R.compose(R.tap(()=>trace("map数组")), square);
  9. isEven = R.compose(R.tap(()=>trace("filter数组")), isEven);
  10. const numbers = _.range(200);
  11. const result = _.chain(numbers)
  12. .map(square)
  13. .filter(isEven)
  14. .take(3)
  15. .value();
  16. console.log(result);
  17. /*
  18. * 首先take(3)只担心前三个通过的map和filter的值
  19. * 其实shortcut fusion技术把map和filter融合
  20. * _.compose(filter(isEven, map(square));
  21. * compose(filter(p1), filter(p2) => filter(x) => p1(x) && p2(x))
  22. * Lodash中其他带由shortcut fusion优化的函数 _.frop...
  23. * _.drop、_.dropRight、_.dropRightWhile、_.dropWhile...
  24. */
  1. //惰性函数
  2. function crreateXHR() {
  3. var xhr = null;
  4. if (typeof XMLHttpRequest != 'undefined') {
  5. xhr = new XMLHttpRequest();
  6. createXHR = function () {
  7. return XMLHttpRequest():
  8. }
  9. } else {
  10. try {
  11. xhr = new ActiveXObject("Msxml2.HMLHTTP");
  12. createXHR = function () {
  13. return new ActiveXObject("Msxml2.XMLHTTP");
  14. }
  15. } catch (e) {
  16. try {
  17. xhr = new ActiveXObject("Microsoft.HMLHTTP");
  18. createXHR = function () {
  19. return new ActiveXObject("Microsoft.XMLHTTP");
  20. }
  21. } catch (e) {
  22. createXHR = function () {
  23. return null;
  24. }
  25. }
  26. }
  27. }
  28. }

高阶函数

函数当参数把传入的函数做一个封装,然后返回这个封装函数,达到更高程度的抽象。
通过代码来看:

  1. let add = function (a, b) {
  2. return a + b;
  3. }
  4. function math(func, array) {
  5. return func(array[0], array[1]);
  6. }
  7. math(add, [1,2]); // => 3
  1. 它是一等公民
  2. 一个函数作为参数
  3. 返回一个函数作为结果

    尾调用优化PTC

    指函数内部的最后一个动作是函数调用。该调用的返回值,直接返回给函数,函数调用自身,成为递归。如果尾调用自身,就称为尾递归。递归需要保存大量的调用记录,很容易发生栈溢出,如果使用尾递归优化,将递归变为循环,那么值需要保存一个调用记录,就不会发生栈溢出。
    1. // 递归 无优化
    2. function factorial(n) {
    3. if(n === 1) return 1;
    4. return n* factorial(n - 1);
    5. }
    6. // 尾递归 有优化
    7. function factorial(n ,total) {
    8. if(n === 1) return total;
    9. return factorial(n - 1, n * total);
    10. }
    尾递归的判断标准是最后一步调用自身,而不是最后一行调用自身,最后一行调用其他函数并返回叫尾调用。

    闭包

    闭包是函数式编程得重要核心,没有闭包就没有函数式编程。 ```javascript function makePowerFn(power) { function powerFn(base){ return Math.pow(base, power); } return powerFn; }

const square = makePowerFn(2); square(3); // => 9

  1. <a name="z2pis"></a>
  2. ## 容器和函子 (Functor)
  3. 范畴其实就是一个容器,里面包含两样东西。值 和 成员变形关系,也就是函数。<br />范畴论使用函数,表达范畴之间得关系。
  4. 函数不仅可以可以用于同一个范畴之中值得转换,还可以用于将一个范畴转成另一个范畴。这就涉及到了函子(Functor)。<br />函子是函数式编程里面最终要得数据类型,也是基本得运算单位和功能单位。它首先是一种范畴,也就是说是一个容器,包含了值和变形关系。比较特殊的是它的变形关系可以一次作用于每一个值,将当前容器变形成另一个容器。<br />函子是遵守特定规则的容器。
  5. 通过代码来看:
  6. ```javascript
  7. const Container = function(x) {
  8. this.value = x;
  9. }

Container 就是一个容器。

把Container变成一个函子:

  1. const Container = function(x) {
  2. this.value = x;
  3. }
  4. Container.of = x => new Container(x);
  5. Container.prototype.map = function(f) {
  6. return Container.of(f(this.value))
  7. }
  8. Container.of(3).map(x => x + 1).map(x => 'Result is' + x);

一般约定,函子的标志就是容器具有map方法。该方法将容器里面的每一个值映射到另一个容器。这里 f 就是变形关系,作用于自己的 value,让后又创建一个新的容器。map每次返回一个 Container ,它把x变成了 Container(4) 和 Container(‘Result is 4’)
ES6这样写:

  1. class Functor{
  2. constroctor(val){
  3. this.val = val;
  4. }
  5. map(f) {
  6. return new Functor(f(this.val));
  7. }
  8. }
  9. (new Functor(2)).map(x => x + 2);

它会返回一个 Functor(4) 。

pointed函子

函子只是实现了map契约的接口。Pointed 函子是一个函子的子集。
生成新的函子的时候,用了 new 命令。这实在不太像函数式编程,因为 new 命令是面向对象编程的标志。函数式编程一般约定,函子又一个 of 方法,用来生成新的容器。

  1. Functor.of = function (val){
  2. return new Functor(val);
  3. }

Maybe函子

Maybe 用于处理错误和异常。函子接受各种函数,处理容器内部的值。这里就有一个问题,容器内部的值可能是一个空值,而外部的函数未必有处理空值的机制,如果传入空值,很可能会出错。

  1. Functor.of(null).map(s => s.toUpperCase());
  2. class Maybe extends Funtor {
  3. map(f) {
  4. return this.val? Maybe.of(f(this.val)):Maybe.of(null);
  5. }
  6. }
  7. Maybe.of(null).mao(s => s.toUpperCase());

Either函子

Either 函子跟 Maybe 比较类似,主要用来 try / catch / throw。
Promise 是可以调用 catch 来集中处理错误的。
事实上 Either 并不是只用来做错误处理的,它表示了逻辑或范畴学里的 coproduc。

  1. class Either extends Functor {
  2. constructor(left, right){
  3. this.left = left;
  4. this.right = right
  5. }
  6. map(f){
  7. return this.right?
  8. Either.of(this.left, f(this.right)):
  9. Either.of(f(this.left), this.right);
  10. }
  11. }
  12. Either.of = function(left, right) {
  13. return new Either(left, right);
  14. }
  15. const addOne = x => x + 1;
  16. Either.of(5, 6).map(addOne); // => Either(6, 7)
  17. Either.of(1, null).map(addOne); // => Either(2, null)
  18. Either.of({address: 'xxx'}, currentUser.address).map(updateFiled);

AP函子

函子里面包含的值,完全可能是函数。我们可以想象一下,一个函子的值是数字,另一个函子的值是函数。AP 函子主要用来解决 value 是函数怎么办。

  1. class Ap extends Functor {
  2. ap(f) {
  3. return Ap.of(this.val(f.val))
  4. }
  5. }

IO函子

真正的程序总是不可能完全纯的。

  1. function readLoacalStorage(){
  2. return window.localStorage;
  3. }

IO跟前面那几个 函子 不同的地方在于,它的 value 是一个函数。 它把不纯的操作(比如 IO、网络请求、DOM)包裹到一个函数内,从而延迟这个操作执行。所以 IO 包含的是被包裹的操作的返回值。
IO也算是惰性求值。
IO负责了调用链积累了很多不纯的操作,带来的复杂性和不可维护性。

  1. import _ from 'lodash';
  2. const compose = _.flowRight;
  3. const IO = function(f) {
  4. this.value = f;
  5. }
  6. IO.of = x => new IO(_ => x);
  7. IO.prototype.map = function(f) {
  8. return new IO(compose(f, this.value));
  9. }

Monad函子

函子是一个容器,可以包含任何值。函子之中再包含一个函子,也是完全合法的。但是,这样就会出现多层嵌套的函子。
Monad 函子的作用是,总是返回一个单层的函子。它就解决了函子嵌套的问题。Pomise就是一种Monad。可以一路 then 下去。

  1. var fs = require('fs');
  2. var _ = require('lodash');
  3. class Functor{
  4. constructor(val) {
  5. this.val = val;
  6. }
  7. static of(val){
  8. return new Functor(val);
  9. }
  10. map(f) {
  11. return new Functor(f(this.val));
  12. }
  13. }
  14. class Monad extends Functor {
  15. join() {
  16. return this.val;
  17. }
  18. flatMap(f) {
  19. /*
  20. * f == 接受一个函数返回的是 IO 函子
  21. * this.val 等于上一步的脏操作
  22. * this.map(f) compose(F, this.val) 函数组合 需要手动执行
  23. * 返回这个函数组合并执行 注意先后的顺序
  24. */
  25. return this.map(f).join();
  26. }
  27. }
  28. var compose = _.flowRight;
  29. class IO extends Monad {
  30. map(f) {
  31. return IO.of(compose(f, this.val));
  32. }
  33. }
  34. var readFile = function (filename) {
  35. return IO.of(function (){
  36. return fs.readFileSync(filename, 'utf-8');
  37. })
  38. }
  39. var print = function (x){
  40. cosnole.log(111);
  41. return IO.of(function (){
  42. console.log(222);
  43. return x + '函数式';
  44. })
  45. }
  46. var tail = function (x){
  47. console.log('tail');
  48. return IO.of(function(){
  49. retrun x + 'tail';
  50. })
  51. }
  52. const result = readFile('./user.txt')
  53. .flatMap(print)()
  54. .flatMap(tail)();
  55. console.log(result().val());