> For the complete documentation index, see [llms.txt](https://chenzhihui.gitbook.io/note/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://chenzhihui.gitbook.io/note/si-xiang/han-shu-shi-bian-cheng.md).

# 函数式编程

函数式编程\
长文预警！！！！！今天给大家分享的主题是最近兴起的一阵热潮——函数式编程，实际上函数式编程的历史，是比面向对象还要久远的，第二古老的高级语言，LISP 正是函数式编程语言的鼻祖；1990年出现的 Haskell 甚至影响了 java 等一大批编程语言的设计，至今仍然是函数式编程语言中无法撼动的王者。\
代码可以进来试试   <https://codesandbox.io/s/reverent-bassi-jpe8d>晨阳推荐的 <https://github.com/getify/Functional-Light-JS> ，不错的书What描述变换为主，采用声明式的方式进行编程。有以下的特点：a. 函数是一等公民，说的即是函数与普通的数字、字符串、对象一样可以被创建、赋值、传递。 甚至连四则运算也应该以函数的形式使用。[为什么使用Ramda](https://adispring.coding.me/2017/06/25/Why-Ramda/)`const r = require('ramda'); // 之后的例子中也会使用 ramda 函数式库const print = r.unary(console.log);[1, 2, 3].forEach(print);`\
`// 顺带一提，我喜欢给对象定义一个 logObject.prototype.log = function(indent){    console.log(JSON.stringify(this, null, indent))}[1,2,3].log();[1,2,3].map(e=>e*2).log();`将函数作为参数传入一个高阶函数，可以利用这种抽象快速的开发出新的功能。`// DRY const sum = function (arr) {  let ret = 0;  for (let v of arr) ret += v;  return ret;}`\
`const product = function (arr) {  let ret = 1;  for (let v of arr) ret *= v;  return ret;}`\
`// Why not trying this?const sum2 = r.reduce(r.add, 0) // 干净简洁const product2 = r.reduce(r.multiply, 1)`reduce实际上提取这种代码的模式，一个初始值，一个用于迭代转换数组元素的函数。函数式编程思维，就是从具体的函数中提取抽象的模式，再通过组合形成具体的函数的过程。\
b. 只有表达式，没有语句。实际上，任何有用的程序都是具有语句的，表达式可以产生值，语句消费产生的值。存在语句就意味着副作用，伴随着IO或者变量状态的改变。我的理解是，产生值的过程中，使用连贯的表达式可以避免多返回类型的危害，这样可以确保结果类型的一致性。实际上ts的联合类型是对js弱类型、不支持重载的妥协。`const doWithStatement = (e) => {    if (e < 10)        return true;    else return 'false'; // 只要存在多个 return 语句，那么类型就可能出现分歧}  // 这只是一个函数，如果分支再多一点，再在内部调用几个别的函数，别的函数也能返回多种类型，复杂度将指数爆炸const doWithExpression = (e) => e < 10;`\
`// const sumColumns = r.pipe(r.transpose, r.map(r.sum)); // initconst matrix = // init     [[1, 2, 3],    [4, 5, 6],    [7, 8, 9]];console.log(sumColumns(matrix)); // [12,15,18]`函数式可以很好地帮助你转换思路，更加简洁地解决问题，也能最大程度地复用已有函数。\
c. 没有副作用，任何纯函数的调用都不应该改变全局（或参数）的状态，或者引起IO，而只是在完成它的工作后返回一个结果值，容器类型必须创建一个新的容器对象`const arr = [-5, 2, -4, 3, 1, 6, 9, -5, -4, -3];const sortByAbs = r.sort(r.ascend(Math.abs)); // 按绝对值排序console.log(sortByAbs(arr));console.log(arr);`\
`// 举个反例  r.concat([x],acc) const reverse_ = r.reduce((acc, x) => acc.unshift(x) && acc , []);const arr2 = r.range(1, 11);console.log(arr2);console.log(reverse(arr2));// << [ 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 ]// unshift是个副作用的函数，当你很自信的写下这行代码，并运行一个通过了之后，就轻易相信了它// 实际上，再调用一次就炸了console.log(reverse(arr2));// << [ 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 ]`其实链表用来表达函数式算法更加合适，至少许多操作不需要复制整个列表。\
d. 不改变状态，这里的理解应该是下了定义就不能轻易的改变它的值，或者是只用常量。（PI就是3.1415926...... ，而不应该是别的什么东西）`const pi = 3.1415926pi = 123456 // error!!!!!`\
f. 引用透明，指的是函数的运行不依赖于外部变量或"状态"，只依赖于输入的参数，任何时候只要参数相同，引用函数所得到的返回值总是相同的。`const square = (e)=>e*elet origin = {x:0,y:0}`\
`const distance = ({x, y})=>Math.sqrt(square(x-origin.x)+square(y-origin.y))console.log(distance({x:3,y:4}))origin.x = origin.y = 10 // don't change the truth of the world！console.log(distance({x:3,y:4})) // shit !!!`<br>

Why

1. 代码简洁，开发快速

`const arr = [1,2,3,4,5,6,7,8,9]arr .map(e=>e*e) // 转换    .filter(e=>e<50) // 过滤    .reduce((a, b)=>a+b,0) // 求和`

1. 接近自然语言，易于理解

`// 判断一个值非空const notNil = r.o(r.not, r.isNil) // r.complement(r.isNil) // 取反`可以看到not 和 isNil 被组合到一起，快速复用，也更具语义，不熟悉函数式的同学可能会写出这样的。`const notNil = (x) => !r.isNil(x);`\
`-- Haskell waynotNil = not . null  -- 好吧这是判断数组非空的`<br>

1. 更方便的代码管理

由于是纯函数，入参相同，返回值也相同，很容易进行单元测试。<br>

1. 易于并行开发

得益于不可变性，代码的执行变得可预测了。<br>

1. 代码的热更新

只要接口相同，就可以直接改良、直接替换。\ <br>

你需要函数式的工具箱[lodash](https://www.lodashjs.com/)其实我一开始接触的是lodash，官网对它的定位是这样的|![](https://bytedance.feishu.cn/space/api/file/out/VMvNIONIOU5IwEdMApFEmeddJsUA6QFXnBZSC10y4bxCmsvgSU/)没错，它是对 js 这门语言本身非侵入性的增强，提供了大量生产力工具函数，你能想到的基本操作但是js不提供的，它基本都有，像什么检查、排序、分组、去重、字符串处理，甚至一些异步的比如节流防抖、延迟……鲁棒性也都不错，比如：`_.map([1,2,3],e=>e*e) // [1,4,9]_.map({a:1,b:2,c:3},e=>e*e) // [1,4,9]_.map(null, e=>e*e) // []_.map(undefined, e=>e*e) // []`一般情况下，能用lodash，我都不会用原生的方法。也正如上文所述，这些方法，难以复用。\
[Ramda](http://ramda.cn/)Ramda是一个纯粹的函数式工具库，你不会在里面看到像lodash那样强的工具函数，但是它提供了创造生产力的能力。Ramda的出现，打破了人们使用js的方式，lodash中数据都是放在第一个参数的，用一次丢一次。ramda干脆地把函数操作的对象，放到了最后，并使用 curry 化。我的理解是，curry化函数的前面的参数，不管有多少，都是服务于最后的参数的，函数使用前面的参数来描述做什么事情，最后的参数才是这件事情的主角。配合curry化，生成的函数就是可复用的。`const compact = r.filter(Boolean) // 清除集合中的假值compact([1,0,'', false, 2, undefined, 4, null]) // [1,2,4]compact({a:1, b:false, c:'', d:'good'}) // {a: 1, d: "good"}`\
`[1,0,'', false, 2, undefined, 4, null].filter(Boolean) // ?????const compact2 = (arr) => arr.filter(Boolean) // 应该关注操作，而不是参数`\
实际上，​lodash/fp​ 几乎搬走了Ramda的所有功能。<br>

> 上面说的大家其实都知道的七七八八，那么函数式编程还有什么特性吗？

函数组合基本上就是管道，不过是从右往左结合的。|![](https://bytedance.feishu.cn/space/api/file/out/Jv1Mdwspjf8tOiZ6OcHGl3wpoHtgEveQyPjW6jmoNMDZq49DFu/)`const r = require("ramda");const compose = (...fns) => initValue =>  r.reduceRight((fn, v) => fn(v), initValue, fns);`\
`const f = compose(  r.multiply(3),  r.add(4));`\
`console.log(f(3)); // （3+4）*3 = 21`\ <br>

递归递归是一种强大的，通过分解、解决子问题并规约的一种计算策略。`// 最大公约数function GCD(a, b) {  return a % b == 0 ? b : GCD(b, a % b);}`\
`-- Haskell 斐波那契数列fib = 0 : 1 : zipWith (+) fib (tail fib)// 0 1 1 2 3 5 8 13 。。。// 1 1 2 3 5 8 13 。。。`柯里化（curry）这其实是[Lambda算子](https://zh.wikipedia.org/wiki/Λ演算)的内容，每个函数实际上只接受一个参数，并返回一个值或函数，这个定义看起来奇怪，其实很有道理，一个函数，参数没接满，调用也没有意义（默认参数？很遗憾，​curry​化不支持默认参数，那并不严谨，显式传参能够让程序更安全），通过柯里化，其返回的函数对象上，就带有了先前的参数信息，为后面的调用提供了便利，多次调用，不需要再重复输入前面的参数了。或者可以这么理解，我们把这个过程看成是一个流水线，装上传送带、发动机、插上电源，最后把原料丢进去。我们所做的就是在给这个流水线做配置，传送带、发动机、电源甚至原料都是可以配置的，而这个流水线提供了一个机制（模式）——如何把这些东西协调起来运作。柯里化就提供了一步一步构建流水线的能力。`-- HaskelladdThree a b c = sum [a, b, c]-- calling ，伟大的Haskell认为给函数调用加上括号完全是多此一举addThree 1 2 3` 在函数式编程中我们需要如此看待标识符。

1. 每个标识符其实都是函数
2. 不需要参数的函数是常量函数
3. 参数接收够了就是一次函数调用

`// javascriptconst addThree = a => b => c => a + b + c const addThree_ = r.curry((a, b, c)=>a+b+c)// callingaddThree(1)(2)(3) // 很无奈addThree_(1,2,3) // 很随意addThree_(1)(2,3)addThree_(1)(2)(3)`\
写这部分内容的时候，我突然醒悟了​curry​化到底意味着什么。

1. 一个标识符，代表着一个已定义的不能改变的固有对象，
2. 参数相同，结果相同的表现是什么？​f(arg1,arg2) === f(arg1,arg2)​?
3. 换一种理解方式，如果参数也是标识符的一部分呢？
4. ​f\_arg1\_arg2 === f\_arg1\_arg2​
5. 当你把参数字面量代入之后，整个标识符都将成为一个定义了的对象
6. 把函数和参数看成一个整体，作为标识符，当我们试图模糊​curry​化函数与其他对象的时候，就会发现它的合理性

`const add3and3 = addThree(3,3)add3and3(6) // 12`

> 所以记忆函数为什么有效？

Free Point Style无参风格，当我们使用高阶柯里化函数构造一个新函数时，如果传入的参数是在最后调用，那么可以省去，简洁许多。`// const sum = arr => r.reduce(r.add, 0, arr)const sum = r.reduce(r.add, 0)`\ <br>

柯里化能够让你的代码更可读、简洁`// normalfetchFromServer()    .then(JSON.parse)    .then(data => data.posts)    .then(`**`posts`** **`=>`**` ``posts.map(post=>post.title)    })`\
`// curry fetchFromServer()    .then(JSON.parse) // 解析JSON    .then(r.`**`get`**`('posts')) // 获取一个字段的数据集    .then(r.map(r.`**`get`**`('title'))) // 取出我们关注的字段`\
\ <br>

高阶函数（high order function）函数可以作为参数传递，也可以作为返回值使用，这就是高阶函数了，同时它也是柯里化的好兄弟，通常一起使用。js代码中经常会看到filter、map、reduce这样的数组方法，单看它们其实并不知道实际要做什么，它们代表了一些通用的操作的抽象。

1. filter过滤，创建新集合并保留一些元素，但是不修改他们
2. map变换，对每个元素做变换再打包成新集合
3. reduce归约，对集合做聚合转换

高阶函数是一些通用的流水线，只有当你提供给它足够的辅助机器，它才知道具体如何运转。这也就是我在上面说的，高阶函数是创造函数的函数。\
对不住。实际上我之前写的算是一个反例了，反省。这有点为了写函数式而写函数式的感觉，js并不适合表达，实际上代码复杂且相当不可读。`// 这里举一个牛顿迭代求平方根的例子// 反省，不要太过追求free point style// number y -> number x -> boolean          |y * y - x| < 0.0001const sqrt_good_enough =  (y, x) => Math.abs(y*y-x) < 0.0001// r.pipe( // 不要再写这种代码，会被打死的//   r.converge(r.subtract,//       [(y) => y * y, r.nthArg(1)]),//   Math.abs,//   r.lt(r.__, 0.0001));`\
`// number y -> number x -> number y'        y' = (y + x/y) / 2const sqrt_improve =  (y, x) => (y + x/y) / 2`\
`// 这里涉及几个过程// 1. 判断迭代结果是不是够好，好就返回// 2. 对y做一次迭代// 3. 返回1步骤const try_it = r.curry(function ti(good_enough, improve, y, x) {  return good_enough(y, x) ? y : ti(good_enough, improve, improve(y, x), x);});`\
`const sqrt = try_it(sqrt_good_enough, sqrt_improve, 1);console.log(sqrt(10));`try\_it高阶函数的表达的意思是，你可以使用任何good\_enough，improve来实现迭代过程。`// 初始值是1 ， 如果不大于100 ，就每次 乘3try_it(r.gt, r.multiply(3), 1, 100); // 243`

惰性求值（lazy evaluation）当你的数据（很大，100万）经过一系列的转换之后，你想要获取其中的前不知道多少个（可能很小），那么前面的转换需要做全吗？`generateList(1,Infinity)    .map(e=>e%2==0?e-3:e+3)    .map(e=>e*e)    .take(10) // 这里取10个，整个流水线，开始运作，但一段只计算10个数出来               // 代码只是随便写写的，实际上不能运行，如有需要，见lazy.js-- In Haskell , this work!take 10 . map （\x -> x*x） . map (\x -> if even x then x-3 else x+3) $ [1..]`这就是惰性求值的使用场景，感受一下就好。<br>

> 一些概念

关于Functor、Applicative、Monad这里有一个介绍的不错的文章<http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html> （下面有中文翻译）\
这几个抽象的概念，简单的说说就是我们使用函数时的一些场景，函子就是数据的容器，比如数组，集合、字典。Functor（函子）我们认为一个容器是​Functor​时，其必须定义一个 ​map​ 实例方法，这个方法将使用一个函数作用于容器中的数据，并将转换后的数据包回​Functor​里面。数组其实就可以看成是函子的一种。`const arr2 = r.range(1,11)const square = r.pipe(r.repeat(r.__,2),r.product)const arr3 = r.map(square, arr2)console.log(arr3)const m1 = {a:1,b:2,c:3}const m2 = r.map(square, m1)console.log(m2)`没了，嗯。\
\
Applicative（应用函子）函子里面是否可以放入函数？这种类型的函子称为 Applicative，要如何将函子里的函数应用到含值的函子上呢？`// javascriptconst random0T200 = r.times(()=>Math.random()*200|0)const maximum = r.reduce(Math.max, Number.NEGATIVE_INFINITY)const minimum = r.reduce(Math.min, Number.POSITIVE_INFINITY)const randomData2 = random0T200(100)const [dataMin, dataMax] = r.ap([minimum, maximum],[randomData2])`\
`-- Haskellarr = [5,2,4,8,9,-1,7][dataMin, dataMax] = [minimum, maximum] <*> [arr]` \
Monad（单子）这是一个令人恐惧的东西，但是想必大家都用过了，没错就是你——​Promise​在Haskell当中，Monad唯一用来写非纯代码的地方，我们可以让Monad去执行一些IO动作（也不仅限IO），当动作完成时，Monad就持有了一个值，它知道它接下来应该做什么（回调），拿出这个值，交给下一个Monad让它继续下一个动作。实际上，在浏览器上面的各种事件模型，都可以看成是Monad。Promise解决了什么问题，回调地狱。`new Promise((res, rej)=>{    ...    res("hello")}).then(str=>{    return new Promise((res, rej)=>{         ...        res(str + " world!")     })}).then(console.log)`可以想象到一连串的 Promise，每个都知道下一个 Promise 是谁，当自己手头的工作做完了，就将结果交给下一个 Promise。\
无比纯粹的单子变换这个地方可能有点儿绕。JavaScript：当我们在写 Promise 的时候，可能就发现了一点，Promise 需要回调，回调的返回，还是Promise，没错，这就是Haskell 大哥教我的单子变换。Haskell：单子接收一个回调，单子通过某种方式取得一个值之后，会拿出这个值，调用回调函数，并保证回调函数一定返回一个单子。在函数式编程语言Haskell中，IO单子是唯一可以执行副作用的对象，而单子变换，保证了这种副作用的纯粹性。JavaScript：为了进一步简化Promise的使用，我创造了await语法糖，但只能在async函数里面调用，作用是从Promise（fulfilled状态）中拿出一个值，然后利用这个值做下一步操作，最后再返回一个Promise（话说 await 和 Haskell 的<- 语法几乎一致）。Haskell：如果你把一个IO单子传递给了一个函数，想从IO单子中拿出值就一定执行了单子变换，那么返回的一定也是一个单子，而返回IO单子的函数可能是不纯的，所以接受返回值为IO单子的函数的函数也是不纯的，故而纯函数一定不接受IO单子作为参数。JavaScript：大哥又在念经了，别晕，听我一言。如果你把 async function作为参数传到另一个函数F里面，调用它并使用await语法拿出来值，那么这个函数F必然也是个async function（因为await只能在async function中使用）。\
\
实际上，用于描述变换行为的概念，不止上面三种。出自 <https://github.com/fantasyland/fantasy-land> ，最近才发现的，Ramda有面向这种些扩展类型的处理机制。|![](https://bytedance.feishu.cn/space/api/file/out/tfL23l9NrL4NK1hTsETuADhzsdKdvVWzquEEIRZey9rMsDrjFm/)\ <br>

> 啥，不够爽？再练习练习？

More Practice <br>

> 给出两个长度相等的数组，在相同的位置上，第一个数组的值作为键，第二个数组的值为值，生成一个字典对象

`const keys = 'this is a example'.split(' ')const values = 'here are some values'.split(' ')`\
`// zipToDict :: [k] -> [v] -> {k:v}const zipToDict = r.pipe(                    r.zip,                    r.fromPairs)           // 转为字典对象console.log(zipToDict(keys, values)) // {this: "here", is: "are", a: "some", example: "values"}`<br>

> 如何用函数式的方式实现 transpose?

`// 命令式编程，你需要显式指定迭代指针 i 和 j 去遍历二维数组const transposeImperative = matrix => {  const cols = matrix[0].length;  const rows = matrix.length;  let res = [];  for (let i = 0; i < rows; i++) {    for (let j = 0; j < cols; j++) {      if (!res[j]) res[j] = [];      res[j][i] = matrix[i][j];    }  }  return res;};`\
`// r.nth(1) 是一个取出数组第1位元素的透镜方法。// nths 是个透镜数组。使用应用函子去映射一个矩阵，就可以一列一列顺序平铺。const transpose = matrix =>{    const cols = matrix[0].length;    const rows = matrix.length;    const nths = r.times(r.nth, cols);    return r.pipe(            r.ap(nths), // 使用了应用函子，结果是一个平铺的数组            r.splitEvery(rows) // 切开           ) (matrix)}`\
`// 也是用到上面的nths，r.__作为占位符可以很方便的留出参数的位置。// 每个 map nth matrix都能提取一列作为结果的一行。const transpose_ = matrix => {    const cols = matrix[0].length    const nths = r.times(r.nth, cols)    return r.map(            r.map(r.__, matrix),             nths) }`\
&#x20;`// 递归方式，挺好理解，就是提取一列作为结果的一行，连接上其余列的转置，就是这js太难看了。 function transpose__(matrix) {  return r.ifElse(    r.isEmpty,    r.always([]),    r.converge(      r.concat,      [        r.o(r.of, r.map(r.head)),        r.compose(transpose__, r.filter(r.has(0)), r.map(r.tail)),      ])  )(matrix)}`\
`// 是时候来两行毁天灭地的 Haskell 代码了transpose [] = []transpose xss = [head xs| xs <- xss]:transpose [xst | xs <- xss, let xst = tail xs, not . null $ xst]`                               &#x20;

> 如何测试呢，尤其在pipeline很长的情况下？

`// 其实可以很容易的在pipeline中间加上输出来观察const longPipe = r.pipe(    r.map(e=>e*e),    r.tap(console.log),    r.map(r.ifElse(        r.lt(r.__,50),        r.add(100),        r.subtract(r.__,200))),    r.tap(console.log),    r.sum,    r.tap(console.log))`\
`longPipe(r.range(1,20))`<br>

> 关于排序，你其实可以更敏捷的构造。

`// 当你有个树形对象o = {    a:1,    b:{        c:1,        d:{e:3}         }}`\
`// 如果我们有形如上面的数组数据，要对其排序，筛选等等// 对于排序，我们需要的，仅仅是如何取到一个key(透镜)，以及是升序还是降序（我只想排序，不想在意是a-b还是b-a）const sortFn = r.sort(r.ascend(r.path(r.split('.','b.d.e')))) //                      decend 降序`<br>

> 反向映射

`const invert = r.pipe(    r.toPairs,    r.map(r.reverse),    r.fromPairs)console.log(invert({ a: 1, b: 2, c: 3 })); // { '1': 'a', '2': 'b', '3': 'c' }`<br>

> 对象扁平化

``const flattenObj = obj => {  const go = obj_ => chain(([k, v]) => {    if (type(v) === 'Object' || type(v) === 'Array') {      return map(([k_, v_]) => [`${k}.${k_}`, v_], go(v)) // 递归go并将前缀k连接上去    } else {      return [[k, v]]    }  }, toPairs(obj_))   // 递归  return fromPairs(go(obj))}flattenObj({a:1, b:{c:3}, d:{e:{f:6}, g:[{h:8, i:9}, 0]}})`` <br>

> 对象键改名，通常会用作数据的适配

`const map = {  "a.b.c": "ab.c",  "a.b.e": "e",  q: "d"};`\
`const obj = {  a: {    b: {      c: 1,      e: 2,      f: 3    }  },  q: 3};`\
`// 技艺不精，写不出更好的代码了const renameKeys = map => {  // [(oldPath, newPath)]  const parsedMap = r.pipe(    r.toPairs,    r.map(r.ap([r.split(".")]))  )(map);`\
&#x20; `return obj =>    r.reduce(      (acc, [oldpath, newpath]) =>        r.pipe(          r.dissocPath(oldpath),          r.assocPath(newpath, r.path(oldpath, acc))        )(acc),      r.clone(obj),       parsedMap    );};`\
`console.log(renameKeys(map)(obj)); // { a: { b: { f: 3 } }, ab: { c: 1 }, e: 2, d: 3 }`\
啥玩意？你还在写这种代码？|![](https://bytedance.feishu.cn/space/api/file/out/3isDtVE3tc7XvIKtf488WfK4XCQKEIGmEeJlD7sypadf7CPVnb/)<br>

总结

1. 大多数程序员都是从类C语言起步的，使用命令式开发可以进行很精细的逻辑控制，要成功的转变编程思维不会很容易。函数式代码，与命令式的不同，就是如何把复杂过程简化、降维，降低思考的负担，减少思考的次数，最大限度的复用代码。
2. 函数式编程提取了过程的模式，并将它们以更加简练的方式表达出来加以复用。
3. 使用组合，与使用无状态的管道过滤器有着惊人的相似性。我们可以直接使用它，或是把它放入其他管道作为一个中间过程。
4. 直线地思考问题，应该避免分支，如果有，也要确保他们能返回一致的类型，最理想的情况是在纯函数中使用表达式。
5. 纯函数应该始终纯净，注意，js 语言本身并不能强迫我们像Haskell 一样将副作用绝对隔离，所以在函数组件当中，不要使用与props无关的数据(非参数变量)，也不应该修改props上的属性。
6. 尽量不要使用null和undefined，他们是面向对象和弱类型语言中最为失败的产物，软件行业已经为此付出了十分惨痛的代价。

> 好吧，是不是应该来点实在的

React 与 函数式编程这个部分我就不写代码了。\
React官方定位是个专注于构建用户界面的库，使用JSX语法，提倡组件化……（废什么话)首先，React的一切，都是组件，尤其开发者们喜欢使用JSX语法来构建出组件（相当符合直觉）。我们可以很容易的通过嵌套组件来往组件上挂子组件，设置props来定制化组件的外观和行为。考虑React组件的生命周期，其实每个组件都有这样一个过程，创建、渲染、挂载、\[渲染、更新]、 卸载。

![](https://bytedance.feishu.cn/space/api/file/out/i4RKDZlj3nYfPsjxHtuMGRbu4X5e0VpfPcPnhPBpgNP3IQko2E/)

创建：我们创建组件使用的语法JSX，实际上创建了一个组件的简易描述，称之React元素，有别于虚拟DOM，虚拟DOM是真正的React组件的实例，有着自己的生命周期，React元素仅仅用于提供给React实例关于组件树的描述、以及传递属性。React运作的过程中，接受React元素，并与React实例比较，如果React实例没有创建出来，那就根据React元素提供的线索（type属性，组件的构造函数），创建出对应的React实例，如果React实例已经存在，就比较他们的类型以及key，如果一致，就将React元素交给React实例，React实例决定是否更新、渲染。渲染：React组件实例中都有一个#render方法，#render的返回，其实也是React元素，返回值给了谁，子组件，React通过这种方式，递归的进行着渲染，渲染可能和大家想象的不太一样，实际上做的是将React元素递归分发给子组件。挂载：React系统使得React组件实例所代表的内容与真实DOM显示一致的过程，称为挂载。掐指一算，嗯，子组件挂载一定先于父组件（只有所有子组件都跟真实DOM一致了，父组件才算挂载完成）。更新：有两种情况会导致组件的更新，一是父组件#render调用，分发React元素给子组件，子组件的props发生改变；二是组件自身调用了#setState，改变了state。props或state发生了变化，就有可能触发更新，取决于#shouldComponentUpdate和#forceUpdate的调用情况，大家肯定比我懂啦。卸载：当父组件自己崩了，或者在#render之后意识到子组件没有与之对应的React元素，就会与子组件断绝关系，即子组件卸载。\
好吧，上面内容是我YY出来的，因为我也没看过源码，但我觉得React大概就是这样运作的。\
说这么多有什么用？我们的主题是函数式编程。从上面的过程中可以看出，React在运作的过程中，组件树的构建、更新，即数据的传递，都是从根组件到叶组件方向进行的。单向数据流：优秀React开发者认为，React应该有两种组件，容器组件与视图组件。容器组件容纳应用的数据、发起网络请求、响应来自视图组件的事件（IOC）；视图组件接受容器组件传来的props数据并展示出来，视图组件上可以有状态、可以有交互，但是对容器组件数据的修改必须以事件的方式通知容器组件（视图组件不应该对容器组件的构成有任何认知，即它是被调用者，它接受容器的数据，但不能改变数据），容器组件响应事件并决定应用状态的改变，容器状态的改变又会反过来重新渲染视图组件，从而视图组件的显示内容与容器组件的相应状态保持一致。这种组件间的交互方式就称为数据下传，动作上传。\
那么这种数据流应该用什么模型表示的呢？万能的Monad，我就知道你按耐不住了。`Component :: [(k, v)] -> DomElement -> IO ()Component status dom = do    (newDom, port) <- render status dom -- 渲染    (end, action) <- listen port        -- 监听        if end          then unMount newDom -- 卸载        else do            let newStatus = reduce status action--根据action和当前的status获得新状态            Component newStatus newDom --状态的改变导致重新渲染，这是一种递归的Monad`\
这大概就是函数响应式了吧。\
\ <br>

我与函数式的孽缘一次偶然，我在图书馆摸到了一本[《Haskell趣学指南》](https://learnyoua.haskell.sg/content/zh-cn/)，从此打开了新世界的大门，这门语言简洁的令人爱不释手，编码如写作，思路如潮涌。如果Scheme是编程语言的女王，那么Haskell应该是国王了。|

![](https://bytedance.feishu.cn/space/api/file/out/p9GR38mEeRLdZcX2cwDnh6v4Yi9L2FVb54LTeEltW36u1qLVGq/)

看过这张图，大概就明白了，Haskell是一门十分强悍的语言。\
模式匹配功能（可以解构赋值）、完全柯里化、强大的高阶函数、可以自定义的运算符、惰性求值、严谨的代数类型系统。样样都走在编程语言的前列。它构造过程的能力异常的强大，尝试接触他，将大大提升一个人的编程思维，看待问题更能抓住本质。\
有人说，Haskell注定只能是学术界的玩具，正是由于它在学术界酝酿多年，才得以快速发展，实际上，已经有一些公司选择用Haskell来开发软件系统了，在安全性上无出其右。<br>
