函数式编程是一种写代码的风格, 灵感来源于数学中的函数, 比如 y=f(x)
函数式编程是声明式的,关注于做什么,而非如何做
用一个简单例子来说明:输出一个数组的每一个元素
const arrayOfWords = ['a', 'b', 'c', 'd']
命令式的代码类似这样
for (let index = 0; index < arrayOfWords.length; index++) {
const word = arrayOfWords[index]
console.log(word)
}
声明式的代码类似这样
arrayOfWords.forEach((word) => console.log(word))
声明式代码的优点:让开发者更关注功能实现,而非繁琐的中间过程
函数具有一些特质
- 必须接受一个参数, 并输出一个结果
- 函数只操作它接受的参数, 并不会对外部产生影响
- 输入相同的x, 输出相同的y
在程序设计领域, 满足以上几点性质的函数被称为纯函数(Pure Functions)
纯函数的特质带来了很多好处
不对外部产生影响(低耦合), 相同的输入有相同的输出(幂等)
这两点特质使得测试纯函数非常容易, 能稳定复现预期结果, 和已有业务逻辑完全解耦
低耦合的特点使得纯函数天然是可以并行执行的, 这可以带来显著的性能提升
在摩尔定律失效,cpu只能堆核心的今天,并行的代码符合潮流趋势
相同输入导致相同输出
这带来了优化的空间, 可以建立一个x-y的键值对表来缓存已有的计算结果, 尤其对于大计算量的操作来说提升非常明显
vue里computed值的设计也遵循了函数式编程**(这也就是为什么在computed里修改外部数据会被eslint报错, 纯函数是不能有副作用的), 免去了重复的dom渲染
react的设计也非常函数式, hooks api usememo(扮演vue里computed值的角色)监视依赖数组的变化来决定是否更新; 纯函数组件即具有纯函数特征的组件, 相同的数据渲染确定的dom
unix哲学: 一个工具干一件事, 复杂功能通过组合来完成
在命令行里经常使用|
来组合命令, 把各个功能用管道接起来, 前者的输入是后者的输出
比如一个任务: 编辑所有当前目录下的配置文件(.zshrc, .npmrc, .vimrc...)
ls -a . | grep '.*rc' | xargs vim
先列出所有文件, 筛选出配置文件, 再用编辑器打开
函数式编程完美符合了这个理念, 一个函数输入x, 输出y, y又可以作为另一个函数的输入
将复杂功能拆分成多个功能单一的纯函数, 通过组合来使用
比如lodash就非常函数式, 这里举个贴近现实一点的例子:
有一堆书,找到某个特定作者写的书, 按价格升序排列
interface Book {
price: number
name: string
author: string
}
const books: Book[] = [
{ price: 12, name: 'a', author: 'aqua' },
{ price: 15, name: 'b', author: 'megumi' },
{ price: 9, name: 'c', author: 'aqua' },
]
import { filter, sortBy } from 'lodash'
const booksByAqua = filter(books, ({ author }) => author === 'aqua')
const booksSortByPrice = sortBy(booksByAqua, ({ price }) => price)
console.log(booksSortByPrice)
输出
[
{ price: 9, name: 'c', author: 'aqua' },
{ price: 12, name: 'a', author: 'aqua' }
]
纯函数的特质使其非常易于组合起来构成复杂功能, 上面的例子呈现了函数的组合使用方式:流
流,就是一个函数的输出可以作为另一个函数的输入
像上面的例子使用了中间变量来存储中间值, 我们也可写成嵌套的风格
sortBy(
filter(books, ({ author }) => author === 'aqua'),
({ price }) => price
)
这种风格有其局限性
- 这在步骤更多的时候会导致更深层的嵌套
- 难以复用, 本质上各个函数构成了一条处理流程, 这种嵌套的写法将处理流程写死了
这催生了一个想法:
如果将每个函数看作一节管道, 依据需求拼接好管线再使用, 就会非常方便
我们可以写一个简单的组合函数
type Step<T> = (_: T) => T
const compose =
<T>(step1: Step<T>, step2: Step<T>) =>
(c: T) =>
step2(step1(c))
const numbers = [1, 2, 3, 4, 5, 6]
const square = (x: number) => x**2
const plusOne = (x: number) => x + 1
const plusOneAndSquare = compose<number>(plusOne, square)
console.log(numbers.map(plusOneAndSquare));
输出
[ 4, 9, 16, 25, 36, 49 ]
现在可以复用plusOneAndSquar
这个操作流程了, 相比之前手动嵌套好了很多
那么是否可以用compose
来处理上面的filter, sortBy
呢?
发现不可行, 这种方式有局限性:
- 只针对单个的参数输入输出, 对于多参函数没法组合
为了解决上面的compose
无法组合具有多参的函数问题,这里引入了 Currying 与 Partial application 两种技巧
翻译过来叫“柯里化”
用一个例子来说明
const multiply = (x: number, y: number) => x * y
type FunctionWithTwoArgs<T> = (arg1: T, arg2: T) => T
const simpleCurry =
<T>(fn: FunctionWithTwoArgs<T>, arg1: T) =>
(arg2: T) =>
fn(arg1, arg2)
const multiplyY = simpleCurry(multiply, 2)
console.log(multiplyY(3))
输出
6
现在就可以用之前的compose
来组合有多参的函数了
const add = (x: number, y: number) => x + y
const addY = simpleCurry(add, 3)
const addAndMultiply = compose(addY, multiplyY)
console.log(addAndMultiply(1))
输出
8
经过currying的多参函数可以分多次接受参数,可以被compose进行组合构成一条管道
针对先固定x, 再接受y的情况 currying 解决了,那么如果需要先固定y再接受x怎么办?
这就暴露了其局限性:
- 入参顺序是确定的, 无法处理多参函数任意顺序的参数填入
为了解决currying的固定入参局限性,这里引入另一种技巧 partial application
用一个例子来说明
const simplePartial = <T>(
fn: (...args: T[]) => T,
...partialArgs: (T | undefined)[]
) => {
const args = partialArgs
return (...restArgs: T[]) => {
let arg = 0
for (let index = 0; index < args.length; index++) {
const partialArg = args[index]
if (partialArg === undefined) args[index] = restArgs[arg++]
}
return fn(...(args as T[]))
}
}
const addX = simplePartial(add, undefined, 3)
const multiplyX = simplePartial(multiply, undefined, 4)
const addAndMultiplyX = compose(addX, multiplyX)
console.log(addAndMultiplyX(1))
输出
16
这里用了undefined
作为占位符,函数在不同位置固定部分参数,之后再接受全部参数
解决了currying的顺序局限性
上面的 compose,simpleCurry, simplePartial 在lodash中都有个更完善的实现flow,curry, partial
下面回到最开始的问题,拼接管道,复用管线,有了下面这个更贴近现实一点的例子
const filterBooksByAlex = partial<Book[], (_: Book) => boolean, Book[]>(
filter,
_,
({ author }) => author === 'Alex'
)
const sortBooks = partial<Book[], (_: Book) => number, Book[]>(
sortBy,
_,
({ price }) => price
)
const bookPipeline = flow(filterBooksByAlex, sortBooks)
const booksA: Book[] = [
{
price: 13,
name: 'b',
author: 'Alex',
},
{
price: 20,
name: 'b',
author: 'Steve',
},
{
price: 17,
name: 'c',
author: 'Alex',
},
]
const booksB: Book[] = [
{
price: 33,
name: 'd',
author: 'Alex',
},
{
price: 30,
name: 'e',
author: 'Steve',
},
{
price: 7,
name: 'f',
author: 'Alex',
},
]
console.log(bookPipeline(booksA))
console.log(bookPipeline(booksB))
输出
[
{ price: 13, name: 'b', author: 'Alex' },
{ price: 17, name: 'c', author: 'Alex' }
]
[
{ price: 7, name: 'f', author: 'Alex' },
{ price: 33, name: 'd', author: 'Alex' }
]