使用函数式编程来避免中间变量和嵌套函数
本文最初发表于coreycleary.me。这是我内容博客的转载文章。我每隔一两周会发布新内容,如果您想直接在邮箱中接收我的文章,可以订阅我的电子报!我还会定期发送速查表和其他免费资源。
在开发一段代码时,我们经常需要获取一个初始值,并对其应用多个函数,然后返回该值。
例如:
const incompleteTasks = getIncomplete(tasks)
const withoutBlockedTasks = getNonBlocked(incompleteTasks)
const sortedByDueDate = sortByDueDate(withoutBlockedTasks)
const groupedByAssignee = groupByAssignee(sortedByDueDate)
// etc...
这样做的问题在于代码难以阅读。每当添加中间变量(例如incompleteTasks`a` withoutBlockedTasks、`b` 等)时,都需要追踪哪些变量被传递给了后续函数。因此,阅读代码时需要进行大量的变量追踪。而且,如果最终没有在其他地方使用到这些中间变量,为什么要创建它们呢?这感觉很浪费。
当然,如果只有几个变量,应该不会对代码的可读性/理解性造成太大影响,但是当需要将初始值传递给很多函数时,很快就会变得混乱不堪,令人头疼。
避免使用中间变量的一种方法是这样做:
groupByAssignee(sortByDueDate(getNonBlocked(getIncomplete(tasks))))
但是,像这样使用嵌套函数会让代码更难读。而且,想在这种代码里添加调试断点简直难上加难!
函数式编程来救场了
使用一种称为函数组合的函数式编程模式,我们可以编写出更易读的代码,而无需中间变量或嵌套函数。
这将使阅读你的代码和审查你的拉取请求的人更容易理解。
现在人人都想用函数式编程——这可是当下最酷的编程方式,而且理由充分。我发现,仅仅通过函数式组合,你就能取得相当大的进展,并获得函数式编程的诸多优势,而无需学习其他更复杂的概念,比如 monad 到底是什么。
所以,你可以把这看作是一举两得!它既能提高代码的可读性,又能让你更多地运用函数式编程。
功能组成
与其先用定义来解释组合,不如我们直接看代码。我们最初的代码(用于获取每次迭代中每个用户剩余的未完成任务数)如下所示:
const { pipe } = require('ramda')
// here are the individual functions, they haven't changed from the above,
// just including them so you can see their implementation
const getIncomplete = tasks => tasks.filter(({complete}) => !complete)
const getNonBlocked = tasks => tasks.filter(({blocked}) => !blocked)
const sortByDueDate = tasks => tasks.sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate))
const groupBy = key => array => {
return array.reduce((objectsByKeyValue, obj) => {
const value = obj[key]
objectsByKeyValue[value] = (objectsByKeyValue[value] || []).concat(obj)
return objectsByKeyValue
}, {})
}
const groupByAssignee = groupBy('assignedTo')
// this is the magic
const getIterationReport = pipe(
getIncomplete,
getNonBlocked,
sortByDueDate,
groupByAssignee
)
很简单,对吧?我们只需把函数放到一个pipe函数里……就完成了!调用这个函数也很简单:
const report = getIterationReport(tasks)
等等,但我以为getIterationReport它是一个变量,而不是一个函数?
这里我们使用了pipe函数式编程库Ramda中的函数。pipe该函数返回一个函数,因此其值getIterationReport实际上也是一个函数。这使得我们可以用任何我们想要的数据来调用它,在本例中是tasks。
因此,函数组合允许我们将函数“链接”在一起以创建另一个函数。就这么简单!我们不必像使用中间变量方法那样存储转换原始数据的每个步骤的结果,而只需定义这些步骤即可。
这:
const getIterationReport = pipe(
getIncomplete,
getNonBlocked,
sortByDueDate,
groupByAssignee
)
比这个好多了:
const getIterationReport = tasks => {
const incompleteTasks = getIncomplete(tasks)
const withoutBlockedTasks = getNonBlocked(incompleteTasks)
const sortedByDueDate = sortByDueDate(withoutBlockedTasks)
return groupByAssignee(sortedByDueDate)
}
构成类型
一般有两种构图方式——compose从右到左pipe和从左到右。pipe
我更喜欢使用这种方式pipe,因为它遵循西方从左到右(或从上到下,就像我们在这里格式化的那样)的阅读标准,并且更容易理解你的数据将如何按顺序通过每个函数。
关于论点
大多数函数pipe和compose实现都只会处理一个参数——用函数式编程术语来说就是“一元函数”。因此,组合最适合那些接受一个值(比如我们tasks这里的函数)并对其进行操作的函数。getIterationReport如果我们除了参数之外还需要传入其他参数,那么我们目前的函数就无法正常工作了tasks。
有一些方法可以改变你的函数来解决这个问题,但这超出了本文的讨论范围。
请注意,如果您使用 Ramda 的管道,第一个函数可以有任意数量的参数,但其余函数必须是一元函数。因此,如果您确实有一个需要多个参数的函数,请将其放在管道的开头pipe。
数据和结果
现在,为了完善整个图景,让我们看一下调用此函数时将要使用的数据:
const tasks = [
{
assignedTo: 'John Doe',
dueDate: '2019-08-31',
name: 'Add drag and drop component',
blocked: false,
complete: false
},
{
assignedTo: 'Bob Smith',
dueDate: '2019-08-29',
name: 'Fix build issues',
blocked: false,
complete: false
},
{
assignedTo: 'David Riley',
dueDate: '2019-09-03',
name: 'Upgrade webpack',
blocked: true,
complete: false
},
{
assignedTo: 'John Doe',
dueDate: '2019-08-31',
name: 'Create new product endpoint',
blocked: false,
complete: false
}
]
调用该函数后,结果如下所示:
{
'Bob Smith': [{
assignedTo: 'Bob Smith',
dueDate: '2019-08-29',
name: 'Fix build issues',
blocked: false,
complete: false
}],
'John Doe': [{
assignedTo: 'John Doe',
dueDate: '2019-08-31',
name: 'Add drag and drop component',
blocked: false,
complete: false
},
{
assignedTo: 'John Doe',
dueDate: '2019-08-31',
name: 'Create new product endpoint',
blocked: false,
complete: false
}
]
}
如您所见,我们筛选掉了已完成和已阻塞的任务,并按负责该任务的开发人员对任务进行了分组。
虽然我们的任务数据结构并不十分复杂,但希望这能帮助您了解我们如何使用组合轻松、清晰地转换数据,而无需使用中间变量来存储转换序列的每个步骤。
所以,下次当你发现自己写出类似这样的代码时:
const incompleteTasks = getIncomplete(tasks)
const withoutBlockedTasks = getNonBlocked(incompleteTasks)
const sortedByDueDate = sortByDueDate(withoutBlockedTasks)
const groupedByAssignee = groupByAssignee(sortedByDueDate)
// etc...
如果你将每一步的结果存储为一个变量,并将该结果传递给下一个函数,那么请使用 Ramda 或你选择的任何库中的 ` composeor`pipe函数,这样代码就更容易阅读和理解了!
如果您觉得这篇文章对您有帮助,这里再次附上订阅我的电子报的链接!
文章来源:https://dev.to/ccleary00/using-functions-programming-to-avoid-intermediate-variables-and-nested-functions-35jj