# 组合与管道
两个理念:
- 每个程序只做好一件事情,为了完成一项新的任务,重新构建要好于在复杂的旧程序中添加新"属性"
- 每个程序的输出应该是另一个尚未可知的程序的输入
# 回顾 map 和 filter
后边会用 compose 来处理这个问题
// 从 apressBooks 数组中获取含有 title 和 author 字段且评级高于 4.5 的对象
let apressBooks = [
{
id: 111,
title: 'C #',
author: 'newming',
rating: [4.7],
reviews: [{good: 4, excellent: 12}]
},
{
id: 222,
title: 'efficient learning machines',
author: 'peter',
rating: [4.5],
reviews: []
},
{
id: 333,
title: 'pro angularjs',
author: 'lack',
rating: [4.0],
reviews: []
},
{
id: 444,
title: 'pro asp.net',
author: 'jack',
rating: [4.2],
reviews: [{good: 14, excellent: 12}]
}
]
// 使用 map 配合 filter 解决问题
map(filter(apressBooks, book => book.rating[0] > 4.5), book => {
return {title: book.title, author: book.author}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# compose 函数实现
const compose = (a, b) => (c) => a(b(c))
1
compose 函数接受两个函数 a, b 作为参数并且返回一个接受一个参数 c 的函数。当用 c 调用返回的函数时,它将用输入 c 调用函数 b,b的输出将作为 a 的输入。
# 应用 compose 函数
- 对给定的数字四舍五入求值。给定的数字为浮点型。
// 传统做法
let data = parseFloat('3.65')
let number = Math.round(data)
// 利用 compose 函数解决问题
let number = compose(Math.round, parseFloat) // 返回一个新的函数,等价于 number = c => Math.round(parseFloat(c))
number('3.68') // 4
1
2
3
4
5
6
7
2
3
4
5
6
7
- 计算一个字符串中单词的数量
let splitIntoSpaces = str => str.split(' ')
let count = array => array.length
// 调用 compose 组合函数
let countWords = compose(count, splitIntoSpaces)
countWords('hello your reading about composition') // 5
1
2
3
4
5
6
2
3
4
5
6
当前 compose 的缺点,只能接受两个函数,并且两个函数都只能接受一个参数
# 引入 curry 和 partial
上边的 compose 存在的问题是仅当函数接受一个参数时,才能将两个函数组合。如果存在多参数的情况,需要使用之前的 curry 和 partial
let apressBooks = [
{
id: 111,
title: 'java',
author: 'newming',
rating: [4.7],
reviews: [{good: 4, excellent: 22}]
},
{
id: 222,
title: 'java script',
author: 'newming123',
rating: [4.5],
reviews: []
},
{
id: 333,
title: 'nodejs',
author: 'happynewming',
rating: [4.0],
reviews: []
}
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
假设现在根据不同评级在代码库中定义了很多小函数用于过滤图书:
// 过滤图书评分
let filterOutStandingBooks = book => book.rating[0] === 5
let filterGoodBooks = book => book.rating[0] > 4.5
let filterBackBooks = book => book.rating[0] < 3.5
// 取出 title author 字段
let projectTitleAndAuthor = book => ({title: book.title, author: book.author})
let projectAuthor = book => ({author: book.author})
let projectTitle = book => ({title: book.title})
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
获取评级高于 4.5 的图书的标题和作者,使用 compose 和 partial 实现:
// 通过 partial 将接受两个参数的函数搞成接受一个函数的函数
let queryGoodBooks = partial(filter, undefined, filterGoodBooks)
let mapTitleAndAuthor = partial(map, undefined, projectTitleAndAuthor)
let titleAndAuthorForGoodBooks = compose(mapTitleAndAuthor, queryGoodBooks)
// 使用
titleAndAuthorForGoodBooks(apressBooks)
1
2
3
4
5
6
7
2
3
4
5
6
7
# 优化 compose
当前的 compose 函数目前只能组合两个给定的函数,接下来使它能接受多个函数。
// 注意这里的 reduce 是 自己实现的 reduce,第一个参数是 array
const compose = (...fns) => value => reduce(fns.reverse(), (acc, fn) => fn(acc), value)
1
2
2
使用优化后的 compose 函数
// 在上面旧的例子的基础上增加一个功能,知道给定字符串的单词数是 奇数 还是 偶数
let splitIntoSpaces = str => str.split(' ')
let count = array => array.length
let oddOrEven = ip => ip % 2 === 0 ? 'even' : 'odd'
// 通过 compose 函数,组合三个函数
const oddOrEvenWords = compose(oddOrEven, count, splitIntoSpaces)
oddOrEvenWords('hello world and go go go!') // even
1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
# 管道/序列
上边的 compose 函数的数据是从右往左流的,下面实现 pipe 函数,功能与 compose 函数相同,只不过数据是从左往右流
# pipe
// 注意这里的 reduce 是 自己实现的 reduce,第一个参数是 array
const pipe = (...fns) => value => reduce(fns, (acc, fn) => fn(acc), value)
1
2
2
# 组合的优势
# 组合满足结合率
compose(f, compose(g, h)) == compose(compose(f, g), h)
1
这样上边的例子就可以这么写了:
let oddOrEvenWords = compose(oddOrEven, compose(count, splitIntoSpaces))
1
# 调用 tap 函数调试
tap 是 underscore.js 中的一个函数,主要目的是在一个链式调用中对中间结果执行某些操作。下面实现 identity 函数,该函数的目的是接受参数并将其返回。
const identity = it => {
console.log(it)
return it
}
1
2
3
4
2
3
4
假设我们有如下的函数调用:
compose(oddOrEven, count, splitIntoSpaces)('Test string')
// 假设执行上面的代码时,如果 count 函数抛出错误,如何得知 count 函数接受到的参数是什么?只需要将 identity 添加到数据流中可能出现错误的位置。
compose(oddOrEven, count, inentity,splitIntoSpaces)('Test string')
1
2
3
4
2
3
4