组合与管道:函数式编程的优雅模式

2.4k words

组合的核心概念

组合模式的核心理念是让每个函数专注于一个小任务,然后通过组合这些函数创建更复杂的功能。这种方法有几个关键优势:

  1. 关注点分离 - 每个函数只负责一个明确的任务
  2. 易于测试 - 小型纯函数更容易进行单元测试
  3. 代码复用 - 可以在不同场景中重复使用这些基础函数 4.可维护性 - 更容易理解和修改

🌰 基础方法与组合方法对比

看一个生成随机整数的例子:

🍐 正常使用

1
2
3
4
5
6
/* 产生随机数,带小数 */
const random = (n) => Math.random() * n
/** 去除小数整化为整数 */
const toInt = (n) => parseInt(n, 10)
// 产生 100之内的整数
const n = toInt(random(100))

使用组合模式,我们可以更优雅地完成相同的任务:

1
2
3
4
5
6
7
8
9
/** 简单的 compose 方法,接受两个方法从右至左执行 */
const compose = (fnLeft, fnRight) => {
return (val) => fnLeft(fnRight(val))
}

// 使用 compose 组合成一个新的方法,产生随机的整数
const randomInt = compose(toInt, random)
// 产生 100之内的整数
console.log(randomInt(100))

这种写法不仅更简洁,还能提高函数的可复用性。

tips: 观察 compose 可知,组成出来的新函数是一元函数, 如果需要组合多元函数. 可使用 柯里化 | 偏函数

🎵 结合律:组合的强大特性

函数组合的一个重要特性是满足结合律,即:

1
2
// 伪代码 二者执行后的结果是完全等价的
compose(compose(f, g), h) === compose(f, compose(g, h))

这意味着组合的顺序是灵活的,只要保持函数执行的顺序不变。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 按空格切割单词 */
const splitIntoSpaces = (str) => str.split(' ')
/** 统计单词数量 */
const countWord = (arr) => arr.length
/** 判断单词数量为奇数还是偶数 */
const oddOrEven = (n) => (n % 2 === 0 ? 'even' : 'odd')

// 根据结合律,fn1与fn2执行结果是完全等价
const fn1 = compose(oddOrEven, compose(countWord, splitIntoSpaces))
const fn2 = compose(compose(oddOrEven, countWord), splitIntoSpaces)

// 执行
const str = 'this is your computer'
console.log(fn1(str)) // even
console.log(fn2(str)) // even

基于以上的特性,开发者可以放心大胆的组合函数。如果需要添加新的功能属性,仅需要再 compose 组合一遍,例如想在函数执行时添加一个打印调试

1
2
3
4
5
// 添加打印
const fn3 = compose(fn2, (it) => {
console.log(it)
return it
})

以上就是 compose 的运行机制,数据流是从右往左(顺序不可变)。还有另外一种数据流的方式从左往右,即管道 pipeline或称序列 sequence

🦌 管道:另一种数据流方向

pipe 单纯是 compose 的复制品,仅仅是改变了数据流的方向

🍐 定义pipe

1
2
3
const pipe = (fnLeft, fnRight) => {
return (val) => fnRight(fnLeft(val))
}

tips: pipe具有compose一样的功能和特性, 二者是等价的。但在程序开发中最好指定其中一种方式,避免数据混乱

💡 | 管道符

unix 系统中,可以使用管道符 | 将结果输入到下一个指令中

1
2
# cat 01.txt 输入文本 => grep 从结果中查询 world 字符
cat 01.txt | grep "world"

🍐 Point Free

将一些对象自带的方法转化为纯函数,不要转瞬即逝的中间变量

1
2
3
4
5
6
7
8
// 🙅🏻‍♀️ 转瞬即逝的变量 `toUpperCase()`
const splitSpaceWord = (str) => str.toUpperCase().split(' ')

// Point Free式做法
const splitSpace = str.split(' ')
const toUpperCase = str.toUpperCase()
/** 使用组合成一个新的函数 */
const splitSpaceWord2 = compose(splitSpace, toUpperCase)

💡 扩展应用

函数组合和管道不仅可用于简单的数据转换,还可应用于:

  • 异步操作链 - 结合Promise处理异步数据流
  • 数据验证链 - 创建一系列验证函数并组合使用
  • 事件处理 - 构建事件处理管道
  • 中间件系统 - 类似Express的中间件模式

实现多函数组合

我们的简单compose只支持两个函数,更实用的组合函数应该支持多个函数

1
2
3
4
5
6
7
8
9
const compose =
(...fns) =>
(initialValue) =>
fns.reduceRight((value, fn) => fn(value), initialValue)

const pipe =
(...fns) =>
(initialValue) =>
fns.reduce((value, fn) => fn(value), initialValue)
Comments