该系列会有 3 篇文章,分别介绍什么是函数式编程、剖析函数式编程库、以及函数式编程在 React 中的应用,欢迎关注我的 blog
命令式编程和声明式编程
拿泡茶这个事例进行区分命令式编程和声明式编程
- 命令式编程
1.烧开水(为第一人称)
2.拿个茶杯
3.放茶叶
4.冲水
- 声明式编程
1.给我泡杯茶(为第二人称)
举个 demo
1 | // 命令式编程 |
什么是函数式编程
函数式编程是声明式编程的范式。在函数式编程中数据在由纯函数组成的管道中传递。
函数式编程可以用简单如
交换律、结合律、分配律
的数学之法来帮我们简化代码的实现。
它具有如下一些特性:
- 纯粹性: 纯函数不改变除当前作用域以外的值;
1 | // 反面示例 |
- 数据不可变性: Immutable
1 | // 反面示例 |
在后记 1 中对数组字符串方法是否对原值有影响作了整理
- 函数柯里化: 将多个入参的函数转化为一个入参的函数;
1 | const add = a => b => c => a + b + c |
- 偏函数: 将多个入参的函数转化成两部分;
1 | const add = a => (b, c) => a + b + c |
- 可组合: 函数之间能组合使用
1 | const add = (x) => x + x |
柯里化(curry)
如下是一个加法函数:
1 | var add = (a, b, c) => a + b + c |
假如有这样一个 curry
函数, 用其包装 add
函数后返回一个新的函数 curryAdd
, 我们可以将参数 a、b
进行分开传递进行调用。
1 | var curryAdd = curry(add) |
动手实现一个 curry 函数
核心思路: 若传进去的参数个数未达到 curryAdd
的个数,则将参数缓存在闭包变量 lists 中:
1 | function curry(fn, ...args) { |
代码组合(compose)
现在有 toUpperCase
、reverse
、head
三个函数, 分别如下:
1 | var toUpperCase = (str) => str.toUpperCase() |
接着使用它们实现将数组末位元素大写化输出, 可以这样做:
1 | var reverseHeadUpperCase = (arr) => toUpperCase(head(reverse(arr))) |
此时在构建 reverseHeadUpperCase
函数的时候, 必须手动声明传入参数 arr, 是否能提供一个 compose
函数让使用者更加友好的使用呢? 类似如下形式:
1 | var reverseHeadUpperCase = compose(toUpperCase, head, reverse) |
此外 compose
函数符合结合律
, 我们可以这样子使用:
1 | compose(compose(toUpperCase, head), reverse) |
以上两种写法与 compose(toUpperCase, head, reverse)
的效果完全相同, 都是依次从右到左执行传参中的函数。
此外 compose
和 map
一起使用时也有相关的结合律, 以下两种写法效果相等
1 | compose(map(f), map(g)) |
动手实现一个 compose 函数
代码精华集中在一行之内, 其为众多开源库(比如 Redux) 所采用。
1 | var compose = (...args) => (initValue) => args.reduceRight((a, c) => c(a), initValue) |
范畴论
范畴论是数学中的一个分支。可以将范畴理解为一个容器, 把原来对值的操作,现转为对容器的操作。如下图:
学习函数式编程就是学习各种函子的过程。
函数式编程中, 函子(Functor)
是实现了 map
函数的容器, 下文中将函子视为范畴,模型可表示如下:
1 | class Functor { |
但是在函数式编程中, 要避免使用 new
这种面向对象的编程方式, 取而代之对外暴露了一个 of
的接口, 也称为 pointed functor
。
1 | Functor.of = value => new Functor(value) |
Maybe 函子
Maybe 函子
是为了解决 this.value
为 null 的情形, 用法如下:
1 | Maybe.of(null).map(r => r.toUpperCase()) // null |
实现代码如下:
1 | class Maybe { |
Either 函子
Either 函子
是为了对应 if...else...
的语法, 即非左即右
。因此可以将之拆分为 Left
和 Right
两个函子, 它们的用法如下:
1 | Left.of(1).map(r => r + 1) // Left {value: 1} |
Left 函子
实现代码如下:
1 | class Left { |
Right 函子
实现代码如下(其实就是上面的 Functor
):
1 | class Right { |
具体 Either
函数只是对调用 Left 函子
或 Right 函子
作一层筛选, 其接收 f
、g
两个函数以及一个函子(Left or Right
)
1 | var Either = function(f, g, functor) { |
使用 demo:
1 | Either((v) => console.log('left', v), (v) => console.log('def', v), left) // left 1 |
Monad 函子
函子会发生嵌套, 比如下面这样:
1 | Functor.of(Functor.of(1)) // Functor { value: Functor { value: 1 } } |
Monad 函子
对外暴露了 join
和 flatmap
接口, 调用者从而可以扁平化嵌套的函子。
1 | class Monad { |
使用方法:
1 | // join |
Monad 函子可以运用在 I/O 这种不纯的操作上将之变为纯函数的操作,目前比较懵懂,日后补充。
后记 1: 数组字符串方法小结(是否对原值有影响)
不会对原数组有影响的方法
slice
1 | var test = [1, 2, 3] |
concat
1 | var test = [1, 2, 3] |
对原数组有影响的方法
splice(这个需要特别记一下)
1 | var test = [1, 2, 3] |
sort
1 | var arr = [2, 1, 3, 4] |
reverse
1 | var test = [1, 2, 3] |
push/pop/unshift/shift
1 | var test = [1, 2, 3] |
不会对原字符串造成影响的方法
substr/substring/slice
1 | // substr |