5

I'm trying to start using free monads in my project and I'm struggling to make it elegant.
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.
For this I need to define different operations for each and combine them into one type using 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.

What I would like to have is User type or stopped computation.
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.

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

Cheers, Leonti

Leonti
  • 10,400
  • 11
  • 43
  • 68
  • If you don't want to handle the error in the `Free` programs, just define `GetUser` as `case class GetUser(id: String) extends UserOp[User]` and let the interpreter handle the error. Similarly for `GetReceipt`. – Tomas Mikula Apr 14 '17 at 04:55
  • @TomasMikula, but I do want to handle error inside of the program, I just want it to be done automatically. Please take a look at this article: https://medium.com/iterators/free-monads-in-web-stack-part-i-2955d44757b5 The guy uses EitherT with Free monad, so when you have an error computation stops automatically without having to unwrap Either. – Leonti Apr 14 '17 at 06:22
  • Yeah, so you want the interpreter to deal with it; you don't want to deal with errors when writing the `Free` programs. That article has `Action`s returning `Either` and then an interpreter `Action ~> Id`. Instead, it could have `Action`s only returning the successful result and then have an interpreter `Action ~> Either[Error, ?]`. No `EitherT` needed, at least not on the user side. This also leaves the error type to be decided by the interpreter. – Tomas Mikula Apr 14 '17 at 07:24

2 Answers2

2

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:

Free[F, A]

Once we add Coproduct of operations into picture, return type becomes:

Free[F[_], A]

, which works fine until we try to transform it to EitherT. If there would not be Coproduct, EitherT would look like:

EitherT[F, ERROR, A]

Where F, is Free[F, A]. But if F is Coproduct and Injection is used, intuition leads to:

EitherT[F[_], ERROR, A]

Which is wrong obviously, here we have to extract type of Coproduct. Which would lead us with kind-projector plugin to:

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

Or with lambda expression:

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))
muradm
  • 1,973
  • 19
  • 30
1

The Freek library implements all the machinery required 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 rediscovered yourself, Free monads and the likes are not extensible without going through the complexity of coproducts. If you are looking for an elegant solution, I would suggest you have a look at Tagless Final Interpreters.

OlivierBlanvillain
  • 7,701
  • 4
  • 32
  • 51
  • Freek seems like a great library. Combining DSLs works great, but I'm still struggling with making `OnionT` work, when I try to do this: `type O = Either[Error, ?] :&: Bulb` I get compiler error `not found: type ?` Here is what I've got so far: https://github.com/Leonti/free-monad-experiment/blob/master/src/main/scala/example/FreeMonadsFreek.scala – Leonti Apr 14 '17 at 09:47
  • I needed to add `kind-projector` plugin for it to work `addCompilerPlugin("org.spire-math" % "kind-projector" % "0.9.3" cross CrossVersion.binary)` It's working great now, exactly what I needed :) I would still like to see a solution with Monad Transformer though, if it's even possible. At this point Freek is pure magic to me :) – Leonti Apr 14 '17 at 10:06