介绍
在过去的几个月里,我一直在潜心研究函数式编程,因为我对 Kotlin 语言非常感兴趣,所以我一直在使用 Arrow 库来玩弄一些东西。
几周前,我一直在研究关于清洁架构的大学客座讲座,在此过程中,我偶然发现了 Mark Seemann 的这篇很棒的博客文章,描述了使用函数式编程如何自动导致清洁架构(或使用一种语言)像 Haskell 一样,编译器甚至可以强制执行 Clean Architecture)。
这激发了我想出一个餐厅预订软件的草稿(如果你有兴趣,检查和构建 repo 应该是轻而易举的)(忠于 Mark Seemann 的领域;))。但是,我不完全确定这个草案中的用例层是否可以称为纯粹的,我希望得到比我自己更多的 FP 经验和知识的人的反馈。
实体层
一个基本用例是尝试为我们餐厅的一定数量的座位创建新的预订。我已经通过以下方式为实体层建模:
fun reservationPossible(
requestedSeats: Int,
reservedSeats: Int,
capacity: Int
): Either<RequestedTooManySeats, ReservationPossible> =
if (reservedSeats + requestedSeats <= capacity) {
ReservationPossible(requestedSeats + reservedSeats).right()
} else {
RequestedTooManySeats.left()
}
const val CAPACITY = 10
object RequestedTooManySeats : Error()
sealed class Error
data class ReservationPossible(val newNumberOfReservedSeats: Int)
这里没有什么太花哨的东西,只是一个检查是否可以预订一定数量的请求座位的功能。一些错误和结果类也在下面以及(为了简单起见)const val
来模拟我们餐厅的容量。
框架/适配器 #1
为了在现实世界的应用程序中有意义,一些数据还需要存储在某种持久层中并从某种持久层中加载。所以,在我们的洋葱架构的最外层,会有一个我为这个例子模拟的数据库:
suspend fun getCurrentlyReservedSeats(): Either<ReadError, Int> {
delay(1) // ... get stuff from db
return 4.right()
}
suspend fun saveReservation(value: String, reservationPossible: ReservationPossible): Either<WriteError, Long> {
delay(1) // ... writing something to db
return 42L.right() // newRecordId
}
abstract class DbError : Error()
object ReadError : DbError()
object WriteError : DbError()
再说一次,这里没有太多事情……只是数据库读/写操作的存根。但是请注意,(按照 Arrow 提出的约定)这些函数用suspend
修饰符标记为不纯函数。
用例
现在来看用例,它基本上描述了我们的应用程序流程:
- 从 DB 获取当前保留的座位数
- 检查请求的座位数量是否仍然可用
- 如果是这样,保留新的保留
- 并返回新创建的预订 ID
它被翻译成reservationUseCase
函数中的代码:
data class UseCaseData(
val requestedSeats: Int,
val reservationName: String,
val getCurrentlyReservedSeats: suspend () -> Either<ReadError, Int>,
val writeVal: suspend (String, ReservationPossible) -> Either<WriteError, Long>,
)
fun reservationUseCase(data: UseCaseData): suspend () -> Either<Error, UseCaseResultData> = {
data.getCurrentlyReservedSeats()
.flatMap { reservationPossible(data.requestedSeats, it, CAPACITY) }
.flatMap { data.writeVal(data.reservationName, it) }
.flatMap { UseCaseResultData(it).right() }
}
data class UseCaseResultData(val newRecordId: Long)
这里是有趣的地方:这个函数接受一些UseCaseData
作为输入,并返回一个suspend
在程序入口处执行的函数,如下所示:
suspend fun main() {
reservationUseCase(
UseCaseData(
requestedSeats = 5,
reservationName = "John Dorian",
::getCurrentlyReservedSeats,
::saveReservation,
)
).invoke().fold(
ifLeft = { throw Exception(it.toString()) },
ifRight = { println(it.newRecordId) },
)
}
所以现在我的问题是:
reservationUseCase
函数本身可以被认为是纯的吗?我读过一些博客文章(不过,以 F# 作为示例语言)建议接收不纯函数作为参数的纯函数可能是纯函数,但不能保证是纯函数。reservationUseCase
在这个例子中,显然确实接收到了不纯的函数UseCaseData
。- 如果它不能被认为是纯粹的,那么如何编写一个像上面在 Kotlin 和 Arrow 中描述的那样的纯粹用例?