4

介绍

在过去的几个月里,我一直在潜心研究函数式编程,因为我对 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 中描述的那样的纯粹用例?
4

1 回答 1

-1

正如您已经假设的那样,严格来说,reservationUseCase 不是一个纯函数。

我看到如何使它成为纯函数的唯一方法是直接传递所有需要的数据,而不是提供对该数据的访问的函数,但我怀疑这是否会使您的代码最终更干净或更易于阅读。

这将得出这样的结论:编排“工作流”的用例函数很少是纯粹的,因为几乎总是需要与某种存储库进行一些交互。

如果您希望某些核心逻辑是纯的,则必须将它们提取到仅接受和返回纯数据的函数中。

于 2021-08-04T16:25:38.447 回答