首页 > 解决方案 > 在功能域设计中使用 Free Monad

问题描述

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

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

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

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

这是真的吗?

我正在使用 Scala 和 Cats 2.2.0。

标签: scalafunctional-programmingscala-catsfree-monad

解决方案


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

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

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

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

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

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

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

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.flatMapinterpretA

然后你添加另一个域,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, ..., *], *]

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

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


推荐阅读