Scala Free monads with coproduct and monad transformer
I am trying to start using free monads in my project and I am trying my best to make it sleek.
Let's say I have two contexts (I actually have more) - Receipt
and User
- both have database operations and I would like to leave their translators separate and compose them at the last moment. To do this, I need to define different operations for each and combine them into one type using Coproduct
.
This is what I got after days of searching 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 understanding has a type Either[Error, User]
that is understandable by looking at the signature getUser
.
What I would like to have is a type User
or a stopped computation.
I know I need to somehow use the monad transformer EitherT
or FreeT
, but after hours of trying, I don't know how to combine the types to make it work.
Can anyone please help? Please let me know if you need more information.
I've also created a minimal sbt project, so anyone who wants to help can run it: https://github.com/Leonti/free-monad-experiment/blob/master/src/main/scala/example/FreeMonads.scala
Cheers, Leonty
source to share
The Freek library implements all the mechanisms you need to solve your problem:
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 rediscover yourself, free monads and the like do not expand without experiencing the complexity of coproducts. If you are looking for an elegant solution, I would suggest you take a look at Odd Finite Interpreters .
source to share
After a 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 the program however 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
}
Few explanations. Without the Coproduct transformer, the functions return:
Free[F, A]
Once we add Coproduct operations to the image, the return type becomes:
Free[F[_], A]
which works great until we try to convert it to EitherT. If there is no Coproduct, EitherT will look like this:
EitherT[F, ERROR, A]
Where F is free [F, A]. But if F is Coproduct and Injection, then intuition leads to:
EitherT[F[_], ERROR, A]
This is wrong, we have to extract the Coproduct type here. Which would lead us with a type-projector plugin to:
EitherT[Free[F, ?], ERROR, A]
Or with a lambda expression:
EitherT[({type L[a] = Free[F, a]})#L, ERROR, A]
This is now the correct type we can hoist with:
EitherT[Free[F, ?], A, B](Free.liftF(I.inj(f)))
We can simplify the return type if needed:
type ResultEitherT[F[_], A] = EitherT[Free[F, ?], Error, A]
And use it in the following functions:
def getReceipt(id: String): ResultEitherT[F[_], ReceiptEntity] = liftFE(GetReceipt(id))
source to share