源码

点击这里前往Github获取本文源码。

背景

在阅读完阮一峰老师的文章Pointfree JavaScript后,突然感觉自己对函数式编程的理解提升了,所以写下这篇文章。

这里的Point指的是函数的实参,所以PointFree就是没有实参的情况下进行函数组合的编程风格。

常规形式

例子来源于Pointfree JavaScript

我们来试着分析一下这段代码干了什么:

1
2
3
4
5
6
7
8
9
function getAdminEmails(users) {
const emails = []
for (const user of users) {
if (user.role === 'admin') {
emails.push(user.email)
}
}
return emails
}
  1. 定义了一个emails数组。
  2. 遍历users数组。
  3. 检查user对象的role属性是否是admin,如果是,就把他的email属性添加到emails数组中。
  4. 返回emails数组。

但如果你细心的话,会发现这个函数名已经说明了一切,它就是获取管理员的邮箱而已。

下面是测试代码,读者可以打开控制台运行:

1
2
3
4
5
6
7
8
const allUsers = [
{ role: 'admin', email: '[email protected]' },
{ role: 'user', email: '[email protected]' },
{ role: 'admin', email: '[email protected]' }
]

console.log(getAdminEmails(allUsers))
// [ '[email protected]', '[email protected]' ]

所以说我们会发现用传统形式编程固然可以达到结果,但是还不是最优解,所以我们试着用一下数组提供的一些函数式编程API。

数组的函数式API

改写一下上面的函数,可以这样:

1
2
3
4
function getAdminEmails(users) {
return users.filter(user => user.role === 'admin')
.map(user => user.email)
}

现在代码十分清晰了,我们就是把role属性为admin的筛选出来,然后获取它们的email属性。

接下来就是这篇文章要说的中心了。

PointFree风格

所谓PointFree风格,就是把功能拆分成非常小的几个点,之后再组合起来,在一切函数调用之前,我们都不需要关心实参是什么,只需要关注自己的逻辑即可。

既然说到组合,不妨先定义一个函数,用来组合一堆函数:

1
2
3
function compose(f, g) {
return x => f(g(x))
}

和数学里接触到的挺像,那么我们接下来要做的事就是考虑getAdminEmails需要哪些组成部分:

  1. 我们要获取邮箱,获取谁的邮箱呢?
  2. 我们需要获取管理员的邮箱。

所以说写下来就是这样的一种形式:

1
2
3
4
const getAdminEmails = compose(
getEmailsOf,
adminUsers
)

那么就可以通过实现这两个函数来组合成我们想要的结果了,先定义getEmailOf

1
2
3
function getEmailsOf(users) {
return users.map(user => user.email)
}

虽然这样写是可以的,但是所谓PointFree风格是想让我们所有的操作都通过一个个函数完成,所以说下面的代码符合风格:

1
2
3
4
5
const prop = key => obj => obj[key]

const map = mapper => array => array.map(mapper)

const getEmailsOf = map(prop('email'))

一开始上来可能有些难理解,但是理解之后就会知道其中的精妙之处了:

  1. prop(key)会返回获取某个对象的key属性的函数。
  2. map(mapper)会返回用mapper映射某个数组的函数。
  3. map(prop('email'))会返回用成员的email属性映射数组的函数。

仿照上面的例子,我们写出adminUsers函数:

1
2
3
4
5
const propIs = value => key => obj => prop(key)(obj) === value

const filter = predicate => array => array.filter(predicate)

const adminUsers = filter(propIs('admin')('role'))

综合起来,最后的代码应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const compose = (f, g) => x => f(g(x))

const prop = key => obj => obj[key]

const propIs = value => key => obj => prop(key)(obj) === value

const map = mapper => array => array.map(mapper)

const filter = predicate => array => array.filter(predicate)

const getAdminEmails = compose(
map(prop('email')),
filter(propIs('admin')('role'))
)

const allUsers = [
{ role: 'admin', email: '[email protected]' },
{ role: 'user', email: '[email protected]' },
{ role: 'admin', email: '[email protected]' }
]

console.log(getAdminEmails(allUsers))
// [ '[email protected]', '[email protected]' ]

读者可以试着到控制台运行一下,查看最终的结果。

我认为这种风格难理解的原因就是它箭头函数用的太多了,让人一下反应不过来,但是细想会觉得这种编程是非常巧妙的,因为最终的函数由一个个小函数组合而成,那么逻辑有问题的时候就可以一个个单独测试这些小函数有没有问题,使得debug更容易。