问题描述
我对函数式编程很陌生。但是,我阅读了有关 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。
无论您选择一个还是另一个,您都不会被迫在任何地方使用它 - 免费的所有内容都可以(应该)解释为特定的实现,无标签让您将特定的实现作为参数传递,因此您可以将其用于单个组件,在其边缘进行解释。