在功能域设计中使用自由Monad



我对函数式编程很陌生。然而,我读到了关于免费Monad的文章,我正试图将其用于一个玩具项目。在这个项目中,我对股票的投资组合域进行建模。正如许多书中所建议的,我定义了PortfolioService的代数和PortfolioRepository的代数。

我想在PortfolioRepository代数和解释器的定义中使用Free monad。目前,我还没有用自由单元来定义PortfolioService代数。

然而,如果我这样做,在PortfolioService解释器中,由于使用的单子不同,我无法使用PortfolioRepository的代数。例如,我不能在同一中使用monadEither[List[String], Portfolio]Free[PortfolioRepoF, Portfolio]进行理解:(

我怀疑,如果我开始使用自由单元来建模一个代数,那么所有需要用它组成的其他代数都必须根据自由单元定义。

这是真的吗?

我使用的是Scala和Cats 2.2.0。

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

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

唯一的区别是你什么时候解释:

  • tagless立即解释,因此它要求您为F传递类型类实例,但由于F是一个类型参数,它给人的印象是它被推迟了,因为它推迟了选择类型的时刻
  • free monad允许您在不依赖于类型类的情况下立即创建值,您可以将它们存储为vals中的objects,不存在对类型类的依赖。你所付出的代价是中间表示,一旦你能够将其解释为有用的结果,你最终想要放弃它。另一方面,它缺少了无标签的能力,即仅将操作约束到某些代数(例如,仅Functor、仅Applicative等,以更好地控制依赖关系中的效果(

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

如果你想学习如何在建模中使用它,Gabriel Volpe的一本书《Scala中的实用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.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, ..., *], *](,它变得更加复杂

在无标签词尾中,总是有一个依赖项,但几乎总是依赖于一种类型——F——许多人的经验证据表明,尽管理论上与自由monad的权力相等,但它更容易使用。但这不是一个科学的论点,所以你可以自己尝试免费的monad。例如,请参阅下面这篇关于同时使用多个DSL的文章。

无论你选择一个还是另一个,你都不会被迫在任何地方使用它-所有免费的东西都可以(应该(被解释为一个特定的实现,无标记使你将特定的实现作为参数传递,这样你就可以将其用于单个组件,即在其边缘进行解释。

最新更新