free-monad-sample
free-monad-sample copied to clipboard
Should define error handing in DSL ?
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 returnUnit
; If validated failed returnError
- The first version DSL of
CaptchaOps
makes me hard to unify their context toFuture[Either[Error, ?]]]
.
If I define error handling in DSL, then I'll face:
- hard to unify their context
- 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 ~> Futurevs
NotifyOps ~> Lambda[A => Future[Either[Error, A]]]. This could be the reason of your error, try build both to
Futureand the
extends CaptchaOps[Either[Error, Unit]]in the case class will provide the
Eitheras the
A` 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