函数式编程,并不是一种特殊的写代码的语法。而是一种特殊的编程思维与理念。函数式编程主要包括如下几个特征。
不可变性指在编程语言中 ,一个对象一旦被创建,就不再发生变化的一种性质。不可变带的主要好处是线程安全和易于推演[1]。
不可变对象上的所有方法,必须是无副作用的。
维基百科是这样定义的:
An expression is called referentially transparent if it can be replaced with its corresponding value (and vice-versa) without changing the program's behavior.
意思是,当一个表达式,如果能被其值所替换,而不改变程序本身的行为,那么我们说这个表达式是引用透明的。函数式编程中常常提到的,无副作用(Side Effects Free)是引用透明的一个推论。
除了无副作用外,引用透明性还要求确定性。即要做到,相同输入函数重复执行总是产出相同的输出,只有这样,才能做到“引用透明”——无论是引用函数调用,还是引用函数调用的结果,不影响程序执行的结果。要做到这一点,还需要把代码自身中的不确定性全部剥离出去。
比如考虑一个订单ID生成器的非函数式和函数式的不同实现方式:
每次执行,其结果都是不同的。于是就不能用方法调用,替代其执行出来的结果。
fun createOrderID(): String {
return "${LocalDate.now()}${UUID.randomUUID()}"
}
只要参数是确定的,那么执行结果就一定是确定的,不因时间、机器的不同而不同。
fun createOrderId(date: LocalDate, seed: String): String {
return date.format(DateTimeFormatter.ISO_DATE) +
UUID.nameUUIDFromBytes(seed.toByteArray(Charsets.UTF_8))
}
函数式的调用方式,会对调用方产生一定的要求,即调用方需要提供这些可变参数的值。这个额外的成本,是否愿意及值得负担,主要取决于调用方是否能从这个函数式风格的模式中获得足够多的回报。如果调用方从整体上来讲,使用的是纯面向对象的设计风格,而且没有多线程等问题,显然是没有办法从中得到足够的收益的。
维基百科里是这样定义“副作用”的[2]:
side effect if it modifies some state variable value(s) outside its local environment, that is to say has an observable effect besides returning a value (the primary effect) to the invoker of the operation.
指对本环境外的变量的变更,换句话说,除了对调用方返回值外的任何可见影响都是副作用。以下是几个副作用的例子:
应当注意的是,有时读操作也会有副作用。比如读文件,操作系统会同步更新文件的最后访问时间。但是当应用对最后访问时间无感的时候,可以视为无副作用。
在面向对象代码中,方法本身有没有副作用,并不是关注点,将副作用穿插在业务逻辑当中是非常自然的选择。但是在函数式编程中,因为会尽量让每个方法纯粹,可组合,无副作用,所以会在逻辑中极力避免副作用。
多数处理用户请求的服务,其流程大体可以分成如下图所示的几个部分:
可以发现每个服务都可以有副作用。而在函数式的风格中,这些副作用会被尽量后置。形成如下图所示的代码结构:
试比较如下所示的两种代码组织结构:
思考一下,如何测试processNote
这个方法。是不是需要mock很多依赖?
class NoteService(
private val noteRepo: NoteRepo,
private val messageBroker: Channel,
) {
fun handleRequest(notes: List<NoteDTO>): AddNoteResponse {
val response = AddNoteResponse()
for (note in notes) {
processNote(note, response)
}
return response
}
private fun processNote(note: NoteDTO, response: AddNoteResponse) {
// side effect
val entity = noteRepo.save(note.toEntity())
// also side effect
messageBroker.publish(note.toEvent())
// mutation of data.
response.Ids.add(entity.id)
}
}
思考一下,如何测试processNote
这个方法。是不是比过程式的简单很多?对业务逻辑的测试,和结合副作用(数据存储及消息发送)的集成测试,是严格分开的。
class NoteService(
private val noteRepo: NoteRepo,
private val messageBroker: Channel,
) {
fun handleRequest(notes: List<NoteDTO>): AddNoteResponse {
val results = notes.map(this::processNote)
// Side Effects are applied as the last steps.
results.map { it.entity }.forEach(noteRepo::save)
results.map { it.event }.forEach(messageBroker::publish)
return AddNoteResponse(results.map { it.entity.id })
}
// All business logic are processed purely without any side effect.
fun processNote(note: NoteDTO): NoteProcessResult {
return NoteProcessResult(note.toEntity(), note.toEvent())
}
internal data class NoteProcessResult(
val entity: NoteEntity,
val event: NoteEvent,
)
}
将副作用延迟的做法,带来的好处是让处理过程更加纯粹可控,但是同时也会有其它的副作用。比如:
这两个问题,都可以通过引入“响应式编程”来缓解,但是同样地,引入“响应式编程”本身,也会带来新的副作用。
函数定义示例。
请注意,使用函数不需要关心函数的内部实现细节。以下代码代码示例,在不考虑内部实现细节的情况下,单单从行为上讲,都可视为纯函数。
fun computeTotalSize(list: List<String>): Int {
var count: Int = 0
for (value in list) {
count += value.length
}
return count
}
并不是说用上一些Lambda风格的代码就是函数式了,这依然是过程式的代码。
fun computeTotalSize(list: List<String>): Int {
var count: Int = 0
list.forEach { value ->
count += value.length
}
return count
}
fun computeTotalSize(list: List<String>) = computeTotalSize(0, list)
private fun computeTotalSize(size: Int, list: List<String>): Int =
if (list.isEmpty())
size
else
computeTotalSize(size + list.first().length, list.drop(1))
或
fun computeTotalSize(list: List<String>): Int {
return list.fold(0) { acc, value -> acc + value.length }
}
或
fun computeTotalSize(list: List<String>) = list.map { it.length }.sum()
或
fun computeTotalSize(list: List<String>) = list.sumBy { it.length }
在面向过程或面向对象的代码中,我们更多地定义可变数据结构。但是在函数式编程风格下,会更倾向使用不可变数据结构。
比如,在一个代码静态分析的类,会统计代码的行数、类数、方法数等统计信息。这个数据结构会有如下几种定义方式:
直接暴露可变的成员。
class CodeStatistics {
var lineCount: Int
var methodCount: Int
var classCount: Int
}
封装成员在几个预定义的操作中。在一般面向对象的领域建模中,会比较常见。
class CodeStatistics {
private var lineCount: Int = 0
fun addLineCount(count: Int) {
lineCount = count + lineCount
}
fun getLineCount(count: Int) {
return lineCount
}
}
每个成员的值都不允许发生变化 。
data class CodeStatistics(
val lineCount: Int = 0,
val methodCount: Int = 0,
val classCount: Int = 0,
)
每个成员的值同样不允许发生变化。但是也会倾向于把行为与数据绑定在一起。
data class CodeStatistics(
val lineCount: Int = 0,
val methodCount: Int = 0,
val classCount: Int = 0,
) {
fun withExtraLine(count: Int): CodeStatistics {
return copy(lineCount = lineCount + count)
}
}
在函数式编程中,函数的执行过程,常常可以通过参数和返回值来控制,而不再仅仅由函数的调用顺序来控制。
NoteRepo对Messenger的依赖是隐式的,其save方法中发送消息是一种副作用。save方法本身,决定了要发消息,并且执行了消息发送这个操作,而且实际控制了发送消息的过程。
@PostMapping("/note/new")
fun saveNote(note: NoteEntity): UUID {
repo.save(note)
return note.id!!
}
class NoteRepo(
private val messenger: Messenger
) {
fun save(entity: NoteEntity): NoteEntity {
saveAndFlush(entity)
val auditEvent = entity.toAuditEvent()
messenger.send(auditEvent)
return entity
}
}
NoteRepo对Messenger的依赖是显式的,这样save方法本身的依赖就更明确了。
@PostMapping("/note/new")
fun saveNote(note: NoteEntity): UUID {
repo.save(messenger, note)
return note.id!!
}
class NoteRepo {
fun save(messenger: Messenger, entity: NoteEntity): NoteEntity {
saveAndFlush(entity)
val auditEvent = entity.toAuditEvent()
messenger.send(auditEvent)
return entity
}
}
save方法不再直接执行消息发送,把执行的权利(责任)让渡给了调用方,同时自己也摆脱了这个副作用,也方便上游调用方延迟执行。
@PostMapping("/note/new")
fun saveNote(note: NoteEntity): UUID {
val messagingIntention = repo.save(note)
messagingIntention(messenger)
return note.id!!
}
class NoteRepo {
fun save(entity: NoteEntity): (Messenger) -> NoteEntity {
saveAndFlush(entity)
val auditEvent = entity.toAuditEvent()
return { it.send(auditEvent) }
}
}
save方法直接将是否需要发送消息的决定权也出让给调用方。同时自身连对Messenger的引用也彻底解开了。
@PostMapping("/note/new")
fun saveNote(note: NoteEntity): UUID {
return repo.save(note) {
messenger.send(it)
it.id!!
}
}
class NoteRepo {
fun <U> save(entity: NoteEntity, callback: (AuditEvent) -> U): U {
saveAndFlush(entity)
val auditEvent = entity.toAuditEvent()
return callback(auditEvent)
}
}
这个代码看更函数式,但是其实是引入不必要的复杂度。没有额外收益,只是单纯地更绕。
repo.save(entity).also { savedRows ->
when (savedRows) {
0 -> logger.error("Failed to save entity: $entity")
}
}
if (repo.save(entity) == 0) {
logger.error("Failed to save entity: $entity")
}
或
val savedRows = repo.save(entity)
if (savedRows == 0) {
logger.error("Failed to save entity: $entity")
}