【问题标题】:Scala Free Monads with Coproduct and monad transformer带有 Coproduct 和 monad 转换器的 Scala Free Monads
【发布时间】:2017-04-14 04:23:00
【问题描述】:

我正在尝试在我的项目中开始使用免费的 monad,但我正在努力让它变得优雅。
假设我有两个上下文(实际上我有更多) - ReceiptUser - 都对数据库进行操作,我希望将它们的解释器分开并在最后一刻组合它们。
为此,我需要为每个操作定义不同的操作,并使用 Coproduct 将它们组合成一种类型。
这是我经过几天的谷歌搜索和阅读后得到的:

  // Receipts
sealed trait ReceiptOp[A]
case class GetReceipt(id: String) extends ReceiptOp[Either[Error, ReceiptEntity]]

class ReceiptOps[F[_]](implicit I: Inject[ReceiptOp, F]) {
  def getReceipt(id: String): Free[F, Either[Error, ReceiptEntity]] = Free.inject[ReceiptOp, F](GetReceipt(id))
}

object ReceiptOps {
  implicit def receiptOps[F[_]](implicit I: Inject[ReceiptOp, F]): ReceiptOps[F] = new ReceiptOps[F]
}

// Users
sealed trait UserOp[A]
case class GetUser(id: String) extends UserOp[Either[Error, User]]

class UserOps[F[_]](implicit I: Inject[UserOp, F]) {
  def getUser(id: String): Free[F, Either[Error, User]] = Free.inject[UserOp, F](GetUser(id))
}

object UserOps {
  implicit def userOps[F[_]](implicit I: Inject[UserOp, F]): UserOps[F] = new UserOps[F]
}

当我想写一个程序时,我可以这样做:

type ReceiptsApp[A] = Coproduct[ReceiptOp, UserOp, A]
type Program[A] = Free[ReceiptsApp, A]

def program(implicit RO: ReceiptOps[ReceiptsApp], UO: UserOps[ReceiptsApp]): Program[String] = {

  import RO._, UO._

  for {
    // would like to have 'User' type here
    user <- getUser("user_id")
    receipt <- getReceipt("test " + user.isLeft) // user type is `Either[Error, User]`
  } yield "some result"
}  

这里的问题是,例如 user in for comprehension 是 Either[Error, User] 类型,查看 getUser 签名是可以理解的。

我想要的是User 类型或停止计算。
我知道我需要以某种方式使用 EitherT monad 转换器或 FreeT,但经过数小时的尝试,我不知道如何组合这些类型以使其工作。

有人可以帮忙吗? 如果需要更多详细信息,请告诉我。

我还在这里创建了一个最小的 sbt 项目,所以任何愿意帮助的人都可以运行它:https://github.com/Leonti/free-monad-experiment/blob/master/src/main/scala/example/FreeMonads.scala

干杯, 莱昂蒂

【问题讨论】:

  • 如果您不想处理Free 程序中的错误,只需将GetUser 定义为case class GetUser(id: String) extends UserOp[User] 并让解释器处理错误。 GetReceipt 也是如此。
  • @TomasMikula,但我确实想处理程序内部的错误,我只想让它自动完成。请看这篇文章:medium.com/iterators/… 这家伙将 EitherT 与 Free monad 一起使用,因此当您遇到错误时,计算会自动停止,而无需打开 Either。
  • 是的,所以您希望解释器处理它;您不想在编写Free 程序时处理错误。那篇文章有Actions 返回Either,然后是一个解释器Action ~&gt; Id。相反,它可以让Actions 只返回成功的结果,然后有一个解释器Action ~&gt; Either[Error, ?]。不需要EitherT,至少在用户端不需要。这也让错误类型由解释器决定。

标签: scala monad-transformers scala-cats free-monad


【解决方案1】:

在与猫长期战斗后:

  // Receipts
sealed trait ReceiptOp[A]
case class GetReceipt(id: String) extends ReceiptOp[Either[Error, ReceiptEntity]]

class ReceiptOps[F[_]](implicit I: Inject[ReceiptOp, F]) {
  private[this] def liftFE[A, B](f: ReceiptOp[Either[A, B]]) = EitherT[Free[F, ?], A, B](Free.liftF(I.inj(f)))

  def getReceipt(id: String): EitherT[Free[F, ?], Error, ReceiptEntity] = liftFE(GetReceipt(id))
}

object ReceiptOps {
  implicit def receiptOps[F[_]](implicit I: Inject[ReceiptOp, F]): ReceiptOps[F] = new ReceiptOps[F]
}

// Users
sealed trait UserOp[A]
case class GetUser(id: String) extends UserOp[Either[Error, User]]

class UserOps[F[_]](implicit I: Inject[UserOp, F]) {
  private[this] def liftFE[A, B](f: UserOp[Either[A, B]]) = EitherT[Free[F, ?], A, B](Free.liftF(I.inj(f)))

  def getUser(id: String): EitherT[Free[F, ?], Error, User] = Free.inject[UserOp, F](GetUser(id))
}

object UserOps {
  implicit def userOps[F[_]](implicit I: Inject[UserOp, F]): UserOps[F] = new UserOps[F]
}

然后你写你想要的程序:

type ReceiptsApp[A] = Coproduct[ReceiptOp, UserOp, A]
type Program[A] = Free[ReceiptsApp, A]

def program(implicit RO: ReceiptOps[ReceiptsApp], UO: UserOps[ReceiptsApp]): Program[Either[Error, String]] = {

  import RO._, UO._

  (for {
    // would like to have 'User' type here
    user <- getUser("user_id")
    receipt <- getReceipt("test " + user.isLeft) // user type is `User` now
  } yield "some result").value // you have to get Free value from EitherT, or change return signature of program 
}  

稍微解释一下。如果没有 Coproduct 转换器,函数将返回:

Free[F, A]

一旦我们将运算的 Coproduct 添加到图片中,返回类型变为:

Free[F[_], A]

,在我们尝试将其转换为 EitherT 之前它工作正常。如果没有 Coproduct,EitherT 看起来像:

EitherT[F, ERROR, A]

其中 F, 是 Free[F, A]。但是如果 F 是 Coproduct 并且使用 Injection,直觉会导致:

EitherT[F[_], ERROR, A]

这显然是错误的,这里我们必须提取 Coproduct 的类型。这将引导我们使用 kind-projector 插件:

EitherT[Free[F, ?], ERROR, A]

或者用 lambda 表达式:

EitherT[({type L[a] = Free[F, a]})#L, ERROR, A]

现在它是正确的类型,我们可以使用:

EitherT[Free[F, ?], A, B](Free.liftF(I.inj(f)))

如果需要,我们可以将返回类型简化为:

type ResultEitherT[F[_], A] = EitherT[Free[F, ?], Error, A]

并在以下功能中使用它:

def getReceipt(id: String): ResultEitherT[F[_], ReceiptEntity] = liftFE(GetReceipt(id))

【讨论】:

    【解决方案2】:

    Freek library 实现了解决您的问题所需的所有机制:

    type ReceiptsApp = ReceiptOp :|: UserOp :|: NilDSL
    val PRG = DSL.Make[PRG]
    
    def program: Program[String] = 
      for {
        user    <- getUser("user_id").freek[PRG]
        receipt <- getReceipt("test " + user.isLeft).freek[PRG]
      } yield "some result"
    

    当您重新发现自己时,如果不经历副产品的复杂性,Free monad 之类的就无法扩展。如果您正在寻找一个优雅的解决方案,我建议您查看Tagless Final Interpreters

    【讨论】:

    • Freek 似乎是一个很棒的库。结合 DSL 效果很好,但我仍然在努力使 OnionT 工作,当我尝试这样做时:type O = Either[Error, ?] :&amp;: Bulb 我得到编译器错误not found: type ? 这是我到目前为止所得到的:github.com/Leonti/free-monad-experiment/blob/master/src/main/…跨度>
    • 我需要添加 kind-projector 插件才能使其工作 addCompilerPlugin("org.spire-math" % "kind-projector" % "0.9.3" cross CrossVersion.binary) 现在工作得很好,正是我需要的 :) 我仍然希望看到 Monad Transformer 的解决方案,如果它是什至的话可能的。在这一点上,Freek 对我来说是纯粹的魔法 :)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-27
    • 1970-01-01
    • 1970-01-01
    • 2019-03-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多