在功能域设计中使用 Free Monad

ric*_*din 1 functional-programming scala free-monad scala-cats

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

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

但是,如果我这样做,在PortfolioService解释器中,PortfolioRepository由于使用的 monad 不同,我无法使用 the 的代数。例如,我不能使用 monads Either[List[String], Portfolio],并且Free[PortfolioRepoF, Portfolio]在同一个for-comprehension 中:(

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

这是真的吗?

我正在使用 Scala 和 Cats 2.2.0。

Mat*_*zok 8

99% 的情况下 Free monad 可以与 Tagless final 互换:

  • 你可以Free[S, *]作为你的Monad实例传递
  • 你可以.foldMap Free[S, A]使用S ~> F映射Monad[F]F[A]

唯一的区别是您何时解释:

  • tagless 立即解释,因此它需要您为您的 传递类型类实例F,但由于它F是一个类型参数,它给人的印象是它被推迟 - 因为它推迟了选择类型的那一刻
  • free monad 允许您立即创建值而不依赖于类型类,您可以将它们存储为vals 中的objects,对类型类没有依赖关系。您付出的代价是中间表示,一旦您能够将其解释为有用的结果,您最终希望将其丢弃。另一方面,它缺少 tagless 将您的操作限制在某些代数上的能力(例如 only Functor, onlyApplicative等,以更好地控制依赖关系中的效果)。

如今,事情变得有利于无标签 final。Free monad 在 IO monad 实现(Cats Effect IO、Monix Task、ZIO)和例如 Doobie 内部使用(尽管我听说 Doobie 的作者正在考虑将其重写为无标签,或者至少后悔没有使用无标签?)。

如果您想学习如何在建模中使用它,可以阅读 Gabriel Volpe - Practical FP in Scala 的一本书,它使用无标签 final 以及我自己的使用 Cats、FS2、Tapir、无标签等的小项目,它们可以展示一些想法。

如果您打算使用 Free,那么有一些挑战:

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 = ...
Run Code Online (Sandbox Code Playgroud)

您可以使用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] = ...
    
    Run Code Online (Sandbox Code Playgroud)
  • 您的服务不能硬编码一个代数,您必须将当前代数注入“更大”的代数:
    def service1[Total[_]](input1: X, input2: Y)(
       implicit inject: InjectK[DomainA, Total]
    ): Free[Total, Z] =
       Free.liftF(inject.inj(Service1(input1, input2)))
    
    Run Code Online (Sandbox Code Playgroud)
  • 你的口译员现在也更复杂了:
    val interpreterTotal: EitherK[DomainA, DomainB, *] ~> IO =
       new (EitherK[DomainA, DomainB, *] ~> IO) {
         def apply[A](fa: EitherK[DomainA, DomainB, A]) =
           fa.run.fold(interpreterA, interpreterB)
       }
    
    Run Code Online (Sandbox Code Playgroud)
  • 并且随着每个新添加的代数 ( EitherK[DomainA, EitherK[DomainB, ..., *], *])变得更加复杂。

在无标签 final 中,总是存在依赖关系,但几乎总是依赖于一种类型——F许多人的经验证据表明,尽管理论上与自由 monad 的权力相同,但它更容易使用。但这不是科学论证,因此您可以随意尝试自己的 free monad。参见例如这篇关于同时使用多个 DSL 的Underscore 文章

无论您选择一个还是另一个,您都不会被迫在任何地方使用它 - 免费的所有内容都可以(应该)解释为特定的实现,无标签让您将特定的实现作为参数传递,以便您可以将其用于单个组件,这是在其边缘解释的。