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

+3


source to share


2 answers


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 .

+1


source


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

      

+1


source







All Articles