scala - 在功能域设计中使用 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。
解决方案
99% 的时间 Free monad 可以与 Tagless final 互换:
Free[S, *]
您可以作为您的Monad
实例传递- 你可以
.foldMap
Free[S, A]
使用S ~> F
映射Monad[F]
到 intoF[A]
唯一的区别是您何时解释:
- tagless 立即解释,因此它要求您为您的 传递类型类实例
F
,但由于F
是类型参数,它给人的印象是它被推迟了 - 因为它推迟了选择类型的时刻 - free monad 可以让你立即创建值,而不依赖于类型类,你可以将它们存储为
val
s inobject
s,不依赖于类型类。您支付的价格是中间表示,一旦您能够解释为有用的结果,您最终想要丢弃它。另一方面,它缺少 tagless 将您的操作限制在某些代数上的能力(例如 onlyFunctor
、 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
.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, ..., *], *]
。
在无标签 final 中总是存在依赖,但几乎总是依赖于一种类型 -F
许多人的经验证据表明,尽管理论上与自由 monad 的权力相同,但它更易于使用。但这不是一个科学论证,所以请随意尝试自己的自由单子。参见例如关于一次使用多个 DSL 的Underscore 文章。
无论您选择一个或另一个,您都不会被迫在任何地方使用它 - 免费的所有内容都可以(应该)解释为特定的实现,无标记使您可以将特定的实现作为参数传递,因此您可以将任何一个用于单个组件,这是在其边缘解释的。