Scala自由Monad与Coproduct和Monad变换器

5
我正在尝试在我的项目中使用自由单子,但我很难做到优雅。
假设我有两个上下文(实际上我有更多) - 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"
}  

问题在于,例如,在 for 推导中的 user 的类型为 Either[Error, User],这个类型可以从 getUser 的签名中理解。
我想要的是 User 类型或停止计算。 我知道我需要以某种方式使用 EitherT monad transformer 或 FreeT,但尝试了几个小时后,我不知道如何组合类型使其正常工作。
有人可以帮忙吗? 如果需要更多细节,请告诉我。
我还创建了一个最小的 sbt 项目,这样任何愿意帮忙的人都可以运行它: https://github.com/Leonti/free-monad-experiment/blob/master/src/main/scala/example/FreeMonads.scala 干杯, Leonti

如果您不想在“Free”程序中处理错误,只需将GetUser定义为case class GetUser(id: String) extends UserOp[User],并让解释器处理错误。对于GetReceipt同样如此。 - Tomas Mikula
@TomasMikula,不过我确实希望在程序内部处理错误,只是希望能够自动完成。请参阅以下文章: https://medium.com/iterators/free-monads-in-web-stack-part-i-2955d44757b5该作者使用了Free monad中的EitherT,因此,当出现错误时,计算会自动停止,而无需解开Either。 - Leonti
是的,你希望解释器来处理它;在编写“Free”程序时不想处理错误。那篇文章中有Action返回Either,然后有一个解释器Action~>Id。相反,它可以只返回成功的结果,并且有一个解释器Action~>Either[Error,?]。用户端至少不需要使用EitherT。这也使得错误类型由解释器决定。 - Tomas Mikula
2个回答

2

经过与猫的长时间斗争后:

  // 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 
}  

一点解释。没有余积变压器,函数会返回以下内容:
Free[F, A]

一旦我们加入操作的和类型,返回类型变为:
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))

1

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"

正如你重新发现的那样,自由单子和类似的东西在不经过余积复杂性的情况下是不可扩展的。如果你正在寻找一种优雅的解决方案,我建议你看看Tagless Final Interpreters


Freek似乎是一个很棒的库。组合DSL非常好用,但我仍然在努力让OnionT工作,当我尝试这样做时:type O = Either[Error, ?] :&: Bulb,我得到了编译器错误not found: type ?。这是我目前的进展: https://github.com/Leonti/free-monad-experiment/blob/master/src/main/scala/example/FreeMonadsFreek.scala - Leonti
我需要添加 kind-projector 插件才能使它工作 addCompilerPlugin("org.spire-math" % "kind-projector" % "0.9.3" cross CrossVersion.binary)。现在它工作得很好,正是我所需要的 :) 不过我仍然想看到一个使用 Monad Transformer 的解决方案,如果可能的话。此时,Freek 对我来说就像是纯魔法 :) - Leonti

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接