简体   繁体   English

Scala免费Monads与Coproduct和monad变压器

[英]Scala Free Monads with Coproduct and monad transformer

I'm trying to start using free monads in my project and I'm struggling to make it elegant. 我正试图在我的项目中开始使用免费的monad,我正在努力让它变得优雅。
Let's say I have two contexts (in reality I have more) - Receipt and User - both have operations on a database and I would like to keep their interpreters separate and compose them at the last moment. 假设我有两个上下文(实际上我有更多) - ReceiptUser - 都在数据库上进行操作,我想让他们的解释器分开并在最后一刻组成它们。
For this I need to define different operations for each and combine them into one type using Coproduct . 为此,我需要为每个操作定义不同的操作,并使用Coproduct将它们组合成一种类型。
This is what I have after days of googling and reading: 这是我在谷歌搜索和阅读几天后所拥有的:

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

When I want to write a program I can do this: 当我想编写程序时,我可以这样做:

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

The problem here is that for example user in for comprehension is of type Either[Error, User] which is understandable looking at the getUser signature. 这里的问题是,例如,用于理解的userEither[Error, User] ,可以理解查看getUser签名。

What I would like to have is User type or stopped computation. 我想要的是User类型或停止计算。
I know I need to somehow use an EitherT monad transformer or FreeT , but after hours of trying I don't know how to combine the types to make it work. 我知道我需要以某种方式使用EitherT monad变换器或FreeT ,但经过几个小时的尝试后,我不知道如何组合这些类型以使其工作。

Can someone help? 有人可以帮忙吗? Please let me know if more details are needed. 如果需要更多详细信息,请与我们联系。

I've also created a minimal sbt project here, so anyone willing to help could run it: https://github.com/Leonti/free-monad-experiment/blob/master/src/main/scala/example/FreeMonads.scala 我还在这里创建了一个最小的sbt项目,所以任何愿意提供帮助的人都可以运行它: https//github.com/Leonti/free-monad-experiment/blob/master/src/main/scala/example/FreeMonads。斯卡拉

Cheers, Leonti 干杯,莱昂蒂

The Freek library implements all the machinery required to solve your problem: Freek库实现了解决问题所需的所有机器:

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"

As you rediscovered yourself, Free monads and the likes are not extensible without going through the complexity of coproducts. 当你重新发现自己时,如果不经历副产品的复杂性,免费的monad和类似的东西是不可扩展的。 If you are looking for an elegant solution, I would suggest you have a look at Tagless Final Interpreters . 如果您正在寻找一个优雅的解决方案,我建议您看看Tagless Final Interpreters

After long battle with Cats: 经过与猫的长期战斗:

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

Then you write program as you want: 然后你可以根据需要编写程序:

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 
}  

A little explanation. 一点解释。 Without Coproduct transformer, functions would return: 没有Coproduct转换器,函数将返回:

Free[F, A]

Once we add Coproduct of operations into picture, return type becomes: 一旦我们将操作的Coproduct添加到图片中,返回类型就变为:

Free[F[_], A]

, which works fine until we try to transform it to EitherT. ,在我们尝试将其转换为EitherT之前,它可以正常工作。 If there would not be Coproduct, EitherT would look like: 如果没有Coproduct,EitherT看起来像:

EitherT[F, ERROR, A]

Where F, is Free[F, A]. 其中F,是自由[F,A]。 But if F is Coproduct and Injection is used, intuition leads to: 但如果F是Coproduct和Injection,则直觉导致:

EitherT[F[_], ERROR, A]

Which is wrong obviously, here we have to extract type of Coproduct. 显然这是错误的,这里我们必须提取Coproduct的类型。 Which would lead us with kind-projector plugin to: 这会引导我们使用kind-projector插件:

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

Or with lambda expression: 或者使用lambda表达式:

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

Now it is correct type to which we can lift with: 现在它是我们可以解除的正确类型:

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

If needed, we can simplify return type to: 如果需要,我们可以将返回类型简化为:

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

And use it in functions like: 并在以下功能中使用它:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM