1. 为什么要学函数式编程
- 函数式编程是非常古老的一个概念,早于第一台计算机的诞生
- 为什么要学习函数
- 函数式编程是随着 React 的流行受到越来越多的关注
- Vue 3 也开始拥抱函数式编程
- 函数式编程可以抛弃 this
- 打包过程中可以更好的利用 tree shaking 过滤无用代码
- 方便测试,方便并行处理
- 有很多库可以帮助我们进行函数式开发:lodash,underScore,ramda
2. 函数式编程
- 概念
- 函数式编程(Functional Programming, FP), FP 是编程范式之一,我们常听说的编程范式还用面向过程编程,面向对象编程
- 面向对象编程的思维方式:把现实世界的事物抽象成程序世界中的对象,通过封装,继承,多态来演示事物事件的联系
- 函数式编程的思维方式:把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)
- 程序的本质:根据输入通过某种运算获得相应的输出,程序开发过程中会涉及很多有输入输出的函数
- x -> f(联系,映射) -> y, y = f(x)
- 函数式编程中的函数指的不是程序中的函数,而是数学中的函数即映射关系,例如:y = sin(x) , x 和 y 的关系
- 相同的输入始终要得到相同的输出(纯函数)
- 函数式编程用来描述数据(函数)之间的映射
3. 函数是一等公民
- 函数可以存储到变量中
- 函数可以作为参数
- 函数可以作为返回值
- 在 JavaScript 中函数就是一个普通的对象(可以通过 new Function()), 我们可以把函数存储到变量/数组中,它还可以作为另一个函数的参数,甚至我们可以在程序运行的时候通过 new Function('alert(1)') 来构造一个函数
4. 高阶函数
- 可以把函数作为参数传递给另一个函数
- 可以把函数作为另一个函数的返回值
- 意义
- 抽象可以帮我们屏蔽细节,只需要关注与我们的目标
- 高阶函数是用来抽象通用的问题
5. 闭包
- 函数和周围的状态(词法环境)的引用捆绑在一起形成闭包
- 可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域的成员
- 本质
- 函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员
- 好处
- 延长了外部函数内部变量的作用范围
6. 纯函数
- 概念
- 相同的输入始终得到相同的输出,而且没有任何的副作用
- 纯函数就类似数学中的函数(用来描述输入和输出之间的关系),y = f(x)
- lodash 是一个纯函数的功能库,提供了对数组,数字,对象,字符串,函数等操作的一些方法
- 数组的 slice 和 splice 分别是:纯函数和不纯的函数
- slice 返回数组中的指定部分,不会改变原数组
- splice 对数组进行操作返回该数组,会改变原数组
- 函数式编程不会保留计算中间的结果,所以变量是不可改变的(无状态的)
- 我们可以把一个函数的执行结果交给另一个函数处理
- 相同的输入始终得到相同的输出,而且没有任何的副作用
- 好处
- 可缓存
- 因为纯函数对相同的输入始终有相同的输出,所以可以把纯函数的结果缓存起来
- 可测试
- 纯函数让测试更方便
- 并行处理
- 在多线程环境上并行操作共享的内存数据很可能会出现意外情况
- 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数
- 可缓存
7. 副作用
- 副作用让一个函数变得不纯,纯函数的根据相同的输入始终得到相同的输出,如果函数依赖外部的状态就无法保证输出相同,就会带来副作用
- 副作用来源
- 全局变量
- 配置文件
- 数据库
- 获取用户输入
- ......
- 所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降不适合扩展和可重用性,同时副作用会给程序中带来安全隐患,给程序带来不稳定性,但是副作用不可能完全禁止,尽可能控制它们在可控制的范围内发生
8. 柯里化
- 使用柯里化解决硬编码的问题
- 概念
- 当一个函数有多个参数的时候,先传递一部分参数调用它(这部分参数以后不会变)
- 然后返回一个新的函数接收剩余的函数,返回结果
- 总结
- 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
- 这是一种对函数参数的缓存
- 让函数变得更灵活,让函数的粒度更小
- 可以把多元函数转变为一元函数,可以组合使用使函数产生强大功能
9. 函数组合
- 纯函数和柯里化很容易写出洋葱代码 h(g(f(x)))
- 获取数组的最后一个元素再转化为大写 _.toUpper(_.first(_.reserve(array)))
- 函数组合可以让我们把细粒度的函数重新组合生成一个新的函数
- 概念
- 如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间的过程的函数合并成一个函数
- 函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
- 函数组合默认是从右向左执行
- lodash 中的组合函数
- lodash 中组合函数 flow() 和 flowRight(),他们都可以组合多个函数
- flow() 是从左向右执行
- flowRight() 是从右向左执行,使用的更多一些
- 函数的组合要满足结合律
- lodash 的 fp 模块提供了实用的对函数式编程友好的方法
- 提供了不可变 auto-curried iteratee-first data-last 的方法
- PointFree
- 我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数
- 不需要指明处理的数据
- 只需要合成运算的过程
- 需要定义一些辅助的基本运算函数
10. 函子
-
什么是函子(Functor)
- 容器:包含值和值的变形关系(这个变形关系就是函数)
- 函子:是一个特殊的容器,通过一个普通的对象实现,该对象具有 map 方法,map 方法可以运行一个函数对值进行处理(变形关系)
-
总结
- 函数式编程的运算不直接操作值,而是由函子完成
- 函子就是一个实现了 map 契约的对象
- 我们可以把函子想象成一个盒子,这个盒子里封装了一个值
- 想要处理盒子中的值,我们需要给盒子的 map 方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理
- 最终 map 方法返回一个包含新值的函子
-
基础函子
class Container { constructor(value) { this._value = value } static of(value) { return new Container(value) } map(fn) { return Container.of(fn(this._value)) } } -
MayBe 函子
class MayBe { constructor(value) { this._value = value } static of(value) { return new MayBe(value) } map(fn) { return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value)) } isNothing() { return this._value === null || this._value === undefined } } -
Either 函子
class Left { constructor(value) { this._value = value } static of(value) { return Left.of(value) } map(fn) { return this } } class Right { constructor(value) { this._value = value } static of(value) { return new Right(value) } map(fn) { return Right.of(fn(this._value)) } } function parseJSON (str) { try { return Right.of(JSON.parse(str)) } catch (err) { return Left.of({ error: err.message }) } } -
IO 函子
-
IO 函子中的 _value 是一个函数,这里是把函数作为值处理
-
IO 函子可以把不纯的动作存储到 _value 中,延迟执行这个不纯的操作(惰性执行),包装当前的操作纯
-
把不纯的操作交给调用者处理
const fp = require('lodash/fp') class IO { constructor(fn) { this._value = fn } static of(value) { return new IO(function () { return value }) } map(fn) { return new IO(fp.flowRight(fn, this._value)) } }
-
-
folktale
-
folktale 是一个标准的函数式编程库
-
和 lodash ramda 不同的是,它没有提供很多的功能
-
只提供了一些函数式处理的操作,例如:compose,curry 等,一些函子 Task,Either,MayBe 等
const { compose, curry } = require('folktale/core/lambda') const { toUpper, first } = require('lodash/fp') let f = curry(2, (x, y) => x + y) console.log(f(2, 3)) console.log(f(2)(3)) let f1 = compose(toUpper, first) console.log(f1(['one', 'tow'])) -
处理异步任务
const { task } = require('folktale/concurrency/task') const fs = require('fs') const { split, find } = require('lodash/fp') function readFile (filename) { return task(resolver => { fs.readFile(filename, 'utf-8', (err, file) => { if(err) { resolver.reject(err) } resolver.resolve(file) }) }) } readFile('./package.json') .map(split('\n')) .map(find(x => x.includes('version'))) .run() .listen({ onReject: err => { console.log(err) }, onResolved: file => { console.log(file) } })
-
-
Pointed 函子
- Pointed 函子是实现了 of 静态方法的函子
- of 方法是为了避免使用 new 来创建对象,更深层的含义是 of 方法用来把值放到上下文的 context(把值放到容器中,使用 map 来处理值)
-
Monad 函子
- Monad 函子是可以变扁的函子
- 一个函子如果具有 join 和 of 两个方法并遵守一些定律就是 Monad