在功能域设计中使用免费 Monad

Using the Free Monad in Functional Domain Design

我对函数式编程还很陌生。但是,我读到了有关 Free Monad 的信息,并且我正尝试在玩具项目中使用它。在这个项目中,我对股票的投资组合域进行建模。正如许多书中建议的那样,我为 PortfolioService 定义了一个代数,为 PortfolioRepository.

定义了一个代数

我想在 PortfolioRepository 代数和解释器的定义中使用 Free monad。现在,我没有根据 Free monad 定义 PortfolioService 代数。

但是,如果我这样做,在 PortfolioService 解释器中,我不能使用 PortfolioRepository 的代数,因为使用的 monad 不同。例如,我不能在同一个 for-comprehension 中使用单子 Either[List[String], Portfolio]Free[PortfolioRepoF, Portfolio] :(

我怀疑如果我开始使用 Free monad 来建模代数,那么所有其他需要与之组合的代数都必须根据 Free monad 来定义。

这是真的吗?

我正在使用 Scala 和 Cats 2.2.0。

99% 的时间 Free monad 可以与 Tagless final 互换:

  • 您可以将 Free[S, *] 作为您的 Monad 实例
  • 您可以 .foldMap Free[S, A] 使用 S ~> FMonad[F] 映射到 F[A]

唯一不同的是你什么时候解读:

  • tagless 立即解释,因此它要求您为 F 传递类型 class 实例,但由于 F 是一个类型参数,它给人的印象是它被延迟了- 因为它推迟了选择类型的时间
  • free monad 让你立即创建值而不依赖于类型 classes,你可以将它们存储为 vals 在 objects 中,没有类型依赖性classes。您支付的价格是您最终希望在能够将其解释为有用结果后立即丢弃的中间表示。另一方面,它缺少无标签的能力,无法将您的操作仅限于某些代数(例如,仅 Functor、仅 Applicative 等,以更好地控制依赖项中的效果)。

如今,一切都倾向于无标签决赛。自由 monad 在内部用于 IO monad 实现(Cats Effect IO、Monix Task、ZIO)和例如Doobie(虽然据我所知,Doobie 的作者正在考虑将其重写为无标签,或者至少后悔没有使用无标签?)。

如果您想学习如何在建模中使用它,可以阅读 Gabriel Volpe 的一本书 - Practical FP in Scala that uses tagless final as well as my own small project 使用 Cats、FS2、Tapir、tagless 等可以展示一些想法。

如果你打算使用免费的,那么好吧,有一些挑战:

sealed trait DomainA[A] extends Product with Serializable
object DomainA {
  case class Service1(input1: X, input2: Y) extends DomainA[Z]
  // ...

  def service1(input1: X, input2: Y): Free[DomainA, Z] =
    Free.liftF(Service1(input1, input2))
}

val interpreterA: DomainA ~> IO = ...

你用Free[DomainA, *],用.map.flatMap等组合,用interpretA解释。

然后您添加另一个域,DomainB。乐趣开始了:

  • 您不能将 Free[DomainA, *]Free[DomainB, *] 结合使用,因为它们是不同的类型,您需要将它们对齐才能实现!
  • 因此,您必须将所有代数合并为一个:
    type BusinessLogic[A] = EitherK[DomainA, DomainB, A]
    implicit val injA: InjectK[DomainA, BusinessLogic] = ...
    implicit val injB: InjectK[DomainB, BusinessLogic] = ...
    
  • 你的服务不能硬编码一个代数,你必须将当前代数注入一个“更大”的代数:
    def service1[Total[_]](input1: X, input2: Y)(
       implicit inject: InjectK[DomainA, Total]
    ): Free[Total, Z] =
       Free.liftF(inject.inj(Service1(input1, input2)))
    
  • 你的解释器现在也更复杂了:
    val interpreterTotal: EitherK[DomainA, DomainB, *] ~> IO =
       new (EitherK[DomainA, DomainB, *] ~> IO) {
         def apply[A](fa: EitherK[DomainA, DomainB, A]) =
           fa.run.fold(interpreterA, interpreterB)
       }
    
  • 每增加一个代数就会变得更加复杂 (EitherK[DomainA, EitherK[DomainB, ..., *], *])。

在 tagless final 中总是存在依赖性,但几乎总是依赖于一种类型 - F - 许多人的经验证据表明尽管理论上与自由 monad 的功能相同,但它更容易使用。但这不是科学论证,所以请随意自己尝试使用免费的 monad。参见例如this Underscore article 关于同时使用多个 DSL。

无论您选择一个或另一个,您都不会被迫在任何地方使用它 - 所有免费的东西都可以(应该)解释为特定的实现,无标签使您可以将特定的实现作为参数传递,因此您可以使用对于单个组件,在其边缘进行解释。