在功能域设计中使用 Free Monad

问题描述

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

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

但是,如果我这样做,在 PortfolioService 解释器中,由于使用了不同的 monad,我无法使用 PortfolioRepository 的代数。例如,我不能在同一个 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 ~> F 映射和Monad[F]F[A]

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

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

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

如果您想学习如何在建模中使用它,可以参考 Gabriel Volpe 的一本书 - Practical FP in Scala 使用无标签 final 以及我的 own small project 使用 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 = ...

您使用 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,*] ~> IO =
       new (EitherK[DomainA,*] ~> IO) {
         def apply[A](fa: EitherK[DomainA,A]) =
           fa.run.fold(interpreterA,interpreterB)
       }
    
  • 随着每个新添加的代数 (EitherK[DomainA,EitherK[DomainB,...,*],*]),它变得更加复杂。

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

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

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...