2017-04-14 49 views
3

我想开始我的项目中使用免费的单子,我挣扎,使其优雅。
比方说,我有两个上下文(在现实中我有更多) - ReceiptUser - 都有一个数据库的操作,我想保持自己的独立口译员,并在最后时刻撰写他们。
为此,我需要为每个操作定义不同的操作,并使用Coproduct将它们合并为一个类型。
这是我在Google上搜寻和阅读的天:斯卡拉免费单子与余积和单子转换

// 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在理解是Either[Error, User]类型,看看getUser签名是可以理解的。

我想有是User型或停止计算。
我知道我需要以某种方式使用EitherT单子转换或FreeT,但尝试,我不知道如何结合的类型,使其工作小时后。

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

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

干杯, Leonti

+0

如果你不想处理的'Free'程序错误,只是定义'GetUser'为'案例类的getUser(ID:字符串)扩展UserOp [用户]'让解释器处理错误。类似于'GetReceipt'。 –

+0

@TomasMikula,但我想处理程序内部的错误,我只是想自动完成。 请看看这篇文章: https://medium.com/iterators/free-monads-in-web-stack-part-i-2955d44757b5 这个家伙使用EitherT配有单子,所以当你有一个错误计算自动停止,无需解开任何一个。 – Leonti

+0

是的,所以你想让翻译处理它;编写“免费”程序时,您不想处理错误。那篇文章有'Action'返回'Either',然后是解释器'Action〜> Id'。相反,它可以有'Action's只返回成功的结果,然后有一个解释器'Action〜>任一[Error,?]'。不需要'EitherT',至少不需要用户方。这也使得错误类型由解释器决定。 –

回答

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

+0

Freek看起来像一个伟大的图书馆。结合DSLs很好,但我仍然努力做'OnionT'工作,当我尝试这样做时: 'type O = [Error,?]:&:Bulb'我得到编译器错误'not found:type ' 这是我到目前为止: https://github.com/Leonti/free-monad-experiment/blob/master/src/main/scala/example/FreeMonadsFreek.scala – Leonti

+0

我需要添加'kind-projector'插件为它工作'addCompilerPlugin(“org.spire-math”%“kind-projector”%“0.9.3”cross CrossVersion.binary)' 现在它工作得很好,正是我需要的:) 尽管如此,我仍然希望看到Monad Transformer的解决方案。在这一点上,Freek对我来说是纯粹的魔术:) – Leonti

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 
} 

一点解释。如果没有余积变压器,函数将返回:

Free[F, A] 

一旦我们增加操作上积成图片,返回类型变为:

Free[F[_], A] 

,直到我们尝试将其转化为EitherT的正常工作。如果不存在Coproduct,则EitherT将如下所示:

EitherT[F, ERROR, A] 

其中F是免费的[F,A]。但是,如果F是余积和注射时,直觉会导致:

EitherT[F[_], ERROR, A] 

哪项是错误很明显,在这里我们要提取上积的类型。这将导致我们种投影机插件:

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))