# 函数式编程

# 函数式编程的思维

  • 函数式编程的一个明显好处是:声明式代码,对于无副作用的纯函数,完全可以不考虑函数内部是如何实现的,专注于编写业务代码,优化代码时,只需集中在这些稳定坚固的纯函数内部即可
  • 对于不纯函数,代码会产生副作用或对外部环境过于依赖,使用他们的时候,总要考虑这些不干净的副作用。系统越大,出现问题的可能就越大

# 范畴

    1. 函数式编程是范畴论的数学分支,他认为世界上所有概念体系都可以抽象出一个个范畴
    1. 彼此之间存在某种关系概念、事务、对象等等,都构成范畴,任何事物只要找出他们之间的关系,就能定义
    1. 箭头表示范畴成员之间的关系,正式的名称叫做“态射”(morphism),范畴论认为,同一范畴的所有成员,就是不同状态的"变形"(transformation)。通过“态射”,一个成员可以变形成另一个成员
    1. 所有的成员是一个集合
    1. 变形关系是函数
    1. 本质上,函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、行列式是同一类东西,都是数学方法
    1. 为什么函数式编程要求函数必须是纯的,不能有副作用?因为它是一种数字运算,原始目的就是求值,不做其他事情,否则就无法满足函数运算法则了

# 基本概念

    1. 函数式编程(Functional Programming)在计算机诞生前就已经存在了,其基础模型来自λ(lambda x=>x*2)演算,而 λ 演算并不是设计于计算机上执行,它是在 20 世纪三十年代引入的一套用于研究函数定义、函数应用和递归的形式系统
    1. 函数式编程的主旨是:将复杂的函数拆分成简单的函数(计算理论,或者递归论、或 lambda 演算)。运算过程尽量写成一系列嵌套的函数调用
    1. 高阶函数是函数式编程的一小部分

# 特点

    1. 函数是一等公民,可以 赋值给其他变量,也可以作为参数,传入另一个函数,或作为别的函数的返回值
    1. 不可改变量。在函数式编程中,变量仅仅代表某个表达式,这里的变量是不能被修改的,而所有的变量只能被赋值一次初始值
    1. map, reduce 是最常用的函数式编程方法
    1. 只用表达式,不用语句
    1. 没有副作用
    1. 不能修改状态
    1. 引用透明(函数运行只能靠参数)

# 函数式编程常用核心概念

# 纯函数

  • 相同的输入,永远得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态
  • 优点:减低系统的复杂度,可缓存
  • 缺点:扩展性差,可以用柯里化解决
var xs = [1, 2, 3, 4, 5, 6, 7];
// Array.slice是纯函数,因为它没有副作用,对于固定的输入,输出总是固定的
xs.slice(0, 3);
xs.slice(0, 3);
xs.splice(0, 3);
xs.splice(0, 3);

# 函数的柯里化

传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数

  • 避免纯函数的值对外界产生依赖
  • 柯里化是一种"预加载"函数的方法,通过传递较少的参数,得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种对参数的”缓存“,是一种高效的编写函数的方法
var checkage = (min) => (age) => age > min;
var checkage18 = checkage(18);
checkage18(20);

function foo(p1, p2) {
  this.val = p1 + p2;
}
// 对参数进行缓存
// 闭包
var bar = foo.bind(null, "p1");
// new的优先级比bind高
var baz = new bar("p2");
console.log(baz.val);
// lodash数组
import { curry } from "lodash";
var match = curry((reg, str) => str.match(reg));
var filter = curry((f, arr) => arr.filter(f));
var haveSpace = match(/\s+/g);
filter(haveSpace)(["sssa", "da asdad  assdf"]);
// 对某个值进行缓存
import _ from "lodash";
var sin = _.memorize((x) => Math.sin(x));
// 第一次要记忆,所以会慢一点
var a = sin(1);
// 第二次有了记忆,所以速度极快
var b = sin(1);

# 函数组合

为了解决柯里化函数嵌套的问题,所以函数组合

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

# Point free

  • 把一些对象自带的方法转换为纯函数,不要命名转瞬即逝的中间变量
  • 这个函数中,我们使用 str 作为中间变量,但这个中间变量除了让代码变长,其他毫无意义
  • 减少一些不必要的命名,让代码保持简洁和通用
const f = (str) => str.toUpperCase().split("");
// 改造
const compose = (f, g) => (x) => f(g(x));
const toUpperCase = (word) => word.toUpperCase();
const split = (x) => (str) => str.split(x);
const f = compose(split(" "), toUpperCase);
f("sada  asdfas sad");

# 声明式与命令式代码

  • 命令式代码:编写一条又一条指令去让计算机执行一些动作,其中包含了很多复杂的细节
  • 声明式代码:通过写表达式的方式来声明我们想干什么,与命令式比,代码可以组合,更加灵活
// 命令式
let ceos = [];
for (var i = 0; i < companies.length; i++) {
  ceos.push(companies[i].ceo);
}
// 声明式
// 可以组合,更加灵活
let ceos = companies.map((c) => c.ceo);

# 惰性求值

在指令式语言中,以下代码会按顺序执行,由于每个函数都有可能改动或依赖于外部的状态,因此必须按顺序执行

function somewhatLongOperation1() {
  dosomewhatLongOperation2;
}

function ajax() {
  var xhr = null;
  if (window.XMLHttpRequest) {
    xhr = new XMLHttpRequest();
  } else {
    xhr = new ActiveXObject("Microsoft.XMLHTTP");
  }
  ajax = xhr;
}

# 高阶函数

函数当做参数,把传入的函数做一个封装,然后返回这个封装函数,达到更高程度的抽象

// 命令式
var add = function(a, b) {
  return a + b;
};
function math(func, array) {
  return func(array[0], array[1]);
}
math(add, [1, 2]);

# 尾调用优化

  • 指函数内部的最后一个动作是函数调用,该调用的返回值直接返回给函数
  • 递归:函数调用自身
  • 尾递归:尾部调用自身
  • 递归需要保存大量的调用记录,很容易发生栈溢出错误
  • 使用尾递归优化,可以将递归变为循环,那么只需保存一个调用记录,这样就不会发生栈溢出错误了(不是尾递归,是无法优化的)
// 不是尾递归
function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}
// 尾递归
function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

普通递归时,内存需要记录调用的堆栈所出的深度和位置信息,在最底层计算返回值,再根据记录的信息,跳回上一层级计算,然后再跳回更高一层,依次执行,直到最外层调用函数,在 CPU 计算和内存会消耗很多,而且当深度过大时,会出现堆栈溢出

function sum(n) {
  if (n === 1) return 1;
  return n + sum(n - 1);
}

sum(5);
//  流的执行记录
5 + sum(4);
5 + 4 + sum(3);
5 + 4 + 3 + sum(2);
5 + 4 + 3 + 2 + sum(1);
5 + 4 + 3 + 2 + 1;
5 + 4 + 3 + 3;
5 + 4 + 6;
5 + 10;
15;

整个计算过程是线性的,调用一次sum(x,total)后,会进入下一个栈,相关数据信息跟随进入,不再放在堆栈上保存,当计算完最后的值之后,直接返回到最上层的sum(5, 0)。这能有效的防止堆栈溢出

function sum(x, total) {
  console.trace(); // 打印调用帧
  if (x === 1) {
    return x + total;
  }
  return #sum(x - 1, x + total);
}

sum(5, 0);
sum(4, 5);
sum(3, 9);
sum(2, 12);
sum(1, 14);
15;

# 浏览器没有尾递归优化

  • 在引擎层面消除递归是一个隐式行为,程序员意识不到
  • 堆栈信息丢失,开发者难以调试
  • 浏览器不支持尾递归,可以把这些递归写成 while
function f1() {
  console.trace();
}
var i = 10;
while (i--) {
  f1();
}
// 浏览器没有尾递归优化
function f1() {
  console.trace(); // 打印调用帧
  throw new Error("xxx");
}
function f2() {
  f1();
}
function f3() {
  f2();
}
f3();

强制浏览器实现尾递归优化

function foo(n) {
  return bar(n * 2);
}
function bar() {
  // 查看调用帧
  console.trace();
}
foo(1);
// 强制指定 只留下bar
// 并不是所有浏览器都支持
return continue
!return
#function()

# 闭包

缓存了当前上下环境的词法作用域
缓存词法作用域是引擎实现的

function makePowerFn(power) {
  // 如果放到外部,powerFn拿不到power
  return function powerFn(base) {
    return Math.pow(base, power);
  };
}
var square = makePowerFn(2);
square(3);

# 容器、Functor

    1. 函数不仅可以用于同一个范畴中值的转换,还可以用于将一个范畴转换为另一个范畴,这就是函子(Functor)
    1. 函子是函数式编程里最重要的数据类型,也是基本运算单位和功能单位。它首先是一种范畴,也就是说,是一个容器,包含了值和变形关系。比较特殊的是,它的变形关系可以依次用于每一个值,将当前容器变形成另一个容器
    1. 函子:可以作用到当前的每一个值,接收一个函数的变形关系,变成另外一个函子(接收一个值,接收一个变形关系,变成另一个值)
    1. 函子是特殊的容器,跟数学里,一堆数通过映射关系函数,变成另一堆数
    1. 函数式编程就是运用不同的函子,来解决实际问题
// 每一个容器都是一个Container
var Container = function(x) {
  this.__value = x;
};
// 函数式编程一般约定,函子有一个of方法
// 新创建一个容器
// 放在of上 是为了看起来不像面向对象
// 面向对象和函子是两个独立的编程范式
Container.of = (x) => new Container(x);
// Container.og("abcd");
// 一般约定,函子的标志是容器具有map方法,该方法将容器里面的每一个值,映射到另一个容器
// f => 变形关系
Container.prototype.map = function(f) {
  return Container.of(f(this.__value));
};
// 返回三个函子
Container.of(3)
  .map((x) => x + 1)
  .map((x) => "result is" + x);

// es6
class Functor {
  constructor(val) {
    this.val = val;
  }
  map(f) {
    // 返回一个新的函子
    return new Functor(f(this.val));
  }
}
// 运算不针对值,而是针对这个值的容器-函子
new Functor(2).map(function(two) {
  return two + 5;
});
// Functor 的 val是7

函子接受各种函数,处理容器内部的值。而容器内部的值可能是一个空值(比如 null),而外部函数未必 有处理空值的机制,如果传入空值,可能会出错

Functor.of(null).map(function(s) {
  return s.toUpperCase();
});
// TypeError
class Maybe extends Functor {
  map(f) {
    return this.val ? Maybe.of(f(this.val)) : Maybe.of(null);
  }
}
Maybe.of(null).map(function(s) {
  return s.toUpperCase();
});

// es5
var Maybe = function(x) {
  this.__value = x;
};
Maybe.of = function(x) {
  return new Maybe(x);
};
Maybe.prototype.map = function(f) {
  return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));
};
Maybe.prototype.isNothing = function() {
  return this.__value === null || this.value === undefined;
};
new Maybe(null).map((v) => v + 3);

# 错误处理、Either

  • try/catch/throw并不是纯的,因为它从外部接管了函数,并且在这个函数出错的时候抛弃了它的返回值throw(e)
  • Promise 可以调用 catch 来集中处理错误
  • Either 并不只是用来做错误处理,它表示了逻辑或,范畴学里的coproduc Either 函子包含两个值:左值(left)和右值(right)。右值是正常的值,左值是右值不存在的时候用的默认值
// 代替try...catch
class Either extends Functor {
  constructor(left, right) {
    this.left = left;
    this.right = right;
  }
  map(f) {
    return this.right
      ? Either.of(this.left, f(this.right))
      : Either.of(f(this.left), this.right);
  }
}
Either.of = function(left, right) {
  return new Either(left, right);
};
var addOne = function(x) {
  return x + 1;
};
Either.of(5, 6).map(addOne);
Either.of(1, null).map(addOne);
Either.of({ address: "xxx" }, currentUser.address).map(updateField);

# AP 函子

    1. 函子里面包含的值,完全可能是函数,一个函子的值是数值,另一个函子的值是函数
    1. ap 就是把对应的函数执行
class Ap extends Functor {
  ap(F) {
    return Ap.of(this.val(F.val));
  }
}
Ap.of(addTwo).ap(Functor.of(2));

# io

  • IO 跟前面那几个Functor不同的地方在于,它的__value是一个函数,它把不纯的操作(比如:IO、网络请求、DOM)包裹到一个函数内,从而延迟这个操作的执行,所以,io 包含的是被包裹的操作的返回值
  • IO 其实也算是惰性求值
  • 集成所有脏操作
import _ from "lodash";
var compose = _.flowRight;

var IO = function(f) {
  this.__value = f;
};
IO.of = (x) => new IO((_) => x);
IO.prototype.map = function(f) {
  return new IO(compose(f, this.__value));
};
import _ from "lodash";
var compose = _.flowRight;
class IO extends Monad {
  map(f) {
    return IO.of(compose(f, this.__value));
  }
}
var fs = require("fs");
var readFile = function(filename) {
  return new IO(function() {
    return fs.readFileSync(filename, "utf-8");
  });
};
readFile("./user.txt") // io的函子
  .flatMap(tail) // this.map.join() == IO.of(compose(tail, fs.readFileSync(filename, "utf-8")))
  .flatMap(print); // IO函子 value => compose(print, tail, fs.readFileSync(filename, "utf-8"))

# 幂等

操作多次 得到同一值

# Monad

# 问题

  • 然后处理嵌套的 Functor(Maybe(IO(42))
  • 如何处理一个由非纯的或者异步的操作序列 need
  • Monad 装箱 拆箱

# Monad

  • Monad 是一种设计模式,表示将一个运算过程,通过函数拆解成相互连接的多个步骤。你只要提供下一步运算所需要的函数,整个运算就会自动进行下去
  • Promise 就是一种 Monad
  • Monad 可以避开嵌套地狱,可以轻松地进行深度嵌套的函数式编程,比如 IO 和其他异步操作
class Monad extends Functor {
  join() {
    // 把io取出来
    return this.val;
  }
  flatMap(f) {
    // f == tail
    return this.map(f).join();
  }
}

# 当下函数式编程最热的库

  • RxJS FRP
  • loadjs

# 函数式编程的实际应用场景

1,函数式编程 不能有 if else 2,范畴论 变形=》太摄 =》变为了一个范畴

-> 固定的输入一定是固定的输出 -> 缓存 -> 柯里化

/// 柯里化之前
function add(x, y) {
  return x + y;
}
add(1, 2);
/// 柯里化之后
function addX(x) {
  return function(y) {
    return x + y;
  };
}
var add8 = addX(8); // 被缓存了
var x = add8(9);
function foo(p1, p2) {
  this.val = p1 + p2;
}
var bar = foo.bind(null, "p1");
var baz = new bar("p2");
console.log(baz.val);
// demo.js
var fs = require("fs");
var _ = require("lodash");
// 基础函子
class Functor {
  constructor(val) {
    this.val = val;
  }
  map(f) {
    return new Functor(f(this.val));
  }
}
// Monad函子
class Monad extends Functor {
  join() {
    return this.val;
  }
  flatMap(f) {
    // 1. f==接受一个函数返回的是IO函子
    // 2. this.val 等于上一步的脏操作
    // 3. this.map(f)  compose(f,this.val)函数组合 需要手动执行
    // 4. 返回这个组合函数并执行,注意先后顺序
    return this.map(f).join();
  }
}
var compose = _.flowRight;
// IO函子用来包裹脏操作
class IO extends Monad {
  // val是最初的脏操作
  static of(val) {
    return new IO(val);
  }
  map(f) {
    return IO.of(compose(f, this.val));
  }
}
var readFile = function(filename) {
  return IO.of(function() {
    return fs.readFileSync(filename, "utf-8");
  });
};
var print = function(x) {
  console.log("111");
  return IO.of(function() {
    console.log("22222");
    return x + "函数式";
  });
};
var tail = function(x) {
  console.log(1111 + x);
  return IO.of(function() {
    return x + "1121212";
  });
};
const result = readFile("./user.txt").flatMap(print);
// .flatMap(print)()
// .flatMap(tail)();
console.log(result().val());
// console.log(result.val());

// demo2.js
var _ = require("lodash");
function square(n) {
  return Math.pow(n, 2);
}
function add(a, b) {
  return a + b;
}
var addSquare = _.flowRight(square, add);
console.log(addSquare(3, 2));