pvillega/free-monad-sample

Should define error handing in DSL ?

Opened this issue · 1 comments

Thank you for writing an excellent article about Free Monad

I try to apply this pattern to my current projects and face a problem.

  • Business flow:
    A user input a correct captcha then system will send a mail.

  • DSL:

    type Error = ???
    sealed trait CaptchaOps[A]
    case class Validate(captcha: String) extends CaptchaOps[Either[Error, Unit]]

    sealed trait NotifyOps[A]
    case class Send() extends NotifyOps[Unit]
  • Interpreters:
      val captchaOpsInterpreter = new (CaptchaOps ~> Future) {
        override def apply[A](fa: CaptchaOps[A]) = ???
      }

      val notifyOpsInterpreter = new (NotifyOps ~> Lambda[A => Future[Either[Error, A]]]) {
        override def apply[A](fa: NotifyOps[A]) = ???
      }
  • Programs:
      import cats.free._

      class CaptchaOpsI[F[_]](implicit I: Inject[CaptchaOps, F]) {
        def validateI(captcha: String): Free[F, Either[Errors, Unit]] =
          Free.inject[CaptchaOps, F](Validate(captcha))
      }

      class NotifyOpsI[F[_]](implicit I: Inject[NotifyOps, F]) {
        def sendI(): Free[F, Unit] = Free.inject[NotifyOps, F](Send(account))
      }

      implicit def captchaOpsI[F[_]](implicit I: Inject[CaptchaOps, F]): CaptchaOpsI[F] = new CaptchaOpsI[F]
      implicit def notifyOpsI[F[_]](implicit I: Inject[NotifyOps, F]): NotifyOpsI[F] = new NotifyOpsI[F]

      type CN[A] = Coproduct[CaptchaOps, NotifyOps, A]

      def program(implicit c: CaptchaOpsI[CN], n: NotifyOpsI[CN]) = {
        import c._
        import n._
        for {
          b <- validateI("right captcha")
          _ <- sendI()
        } yield {
          "Success !!!"
        }
      }

      val interpreter = captchaOpsInterpreter or notifyOpsInterpreter

      val result = program.foldMap(interpreter)

I can't compose captchaOpsInterpreter and notifyOpsInterpreter altogether because their context is not the same.

The context of captchaOpsInterpreter is Future
The context of notifyOpsInterpreter is Future[Either[Error, ?]]]

I try to unify the context and use EitherT

      val captchaOpsInterpreter = new (CaptchaOps ~> EitherT[Future, Error, ?]) {
        override def apply[A](fa: CaptchaOps[A]) = fa match {
          case Validate(captcha: IdentityCaptcha) =>
            EitherT.right[Future, Error, A](Future(().asInstanceOf[A]))
        }
      }

      val notifyOpsInterpreter = new (NotifyOps ~> EitherT[Future, Errors, ?]) {
        override def apply[A](fa: NotifyOps[A]) = fa match {
          case Send(account: BusinessEmailAccount) =>
            EitherT.right[Future, Error, A](Future(().asInstanceOf[A]))
        }
      }

Passed compiling check but I got a runtime error

java.lang.ClassCastException: scala.runtime.BoxedUnit cannot be cast to scala.util.Either

I find out that Captcha DSL I defined makes me hard to unify their context to Future[Either[Error, ?]]]

I modify Captcha DSL and can unify their context easily:

    sealed trait CaptchaOps[A]
    case class Validate(captcha: String) extends CaptchaOps[Unit]

Question:

  • The last version DSL of CaptchaOps doesn't express the intention: If validated successfully return Unit; If validated failed return Error
  • The first version DSL of CaptchaOps makes me hard to unify their context to Future[Either[Error, ?]]].

If I define error handling in DSL, then I'll face:

  1. hard to unify their context
  2. need Monad transformer to get result

If I don't define error handling in DSL, I'm afraid that DSL doesn't express its intention.

I want to know your suggestion :>

Hi, first of all apologies for the delay, I just noticed this!

First of all, I'm wondering if you can't compose the initial interpreters because you are using different types on the natural transformation: CaptchaOps ~> FuturevsNotifyOps ~> Lambda[A => Future[Either[Error, A]]]. This could be the reason of your error, try build both to Futureand theextends CaptchaOps[Either[Error, Unit]]in the case class will provide theEitheras theA` received in the natural transformation. Should work.

Second, as an alternative to compose languages, look at https://github.com/ProjectSeptemberInc/freek . It will simplify the boilerplate and the examples may help you build the stack you want

Cheers