Last active
September 4, 2018 12:56
-
-
Save afsalthaj/b5232ca198e27c369e899c2d83b6de2e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.telstra.dxmodel.api.interop | |
import cats.data.{EitherT, Kleisli, Writer, WriterT} | |
import cats.free.Free | |
import cats.implicits._ | |
import cats.~> | |
import scalaz.zio.IO | |
import com.telstra.dxmodel.instances.AllMonadInstances._ | |
// Sketch: Please don't delete until the whole app works with this interpeter: | |
// Technique to build a computation description which should accumulate info on each execution step | |
// which is to be returned to the user regardless of failure or success. | |
// In other words, instead of `Left[Error] \/ Right[List[StepsExecuted]]]`, | |
// we get `(List[StepsExecuted], Left[Error])` using EitherT[WriterT[IO[...]]] while **failing early**. | |
// The gist is, in particular, handle Free monads and shows how nice it is | |
// compared to finally tagless where transformers can come all over the place of business logic. | |
// PS: | |
// There should be a cats monad instance for zio since I am using cats: | |
// {{{ | |
// implicit def catsMonad[E]: cats.Monad[IO[E, ?]] = new cats.Monad[IO[E, ?]]{ | |
// override def flatMap[A, B](fa: IO[E, A])(f: A => IO[E, B]): IO[E, B] = fa.flatMap(f) | |
// override def tailRecM[A, B](a: A)(f: A => IO[E, Either[A, B]]): IO[E, B] = | |
// f(a).flatMap { | |
// case Left(l) => tailRecM(l)(f) | |
// case Right(r) => IO.now(r) | |
// } | |
// override def pure[A](x: A): IO[E, A] = IO.point(x) | |
// } | |
// | |
// }}} | |
// Also I have used my own `asIO` to convert zio to cats for easily running without extending RTS. | |
// You may please choose to run with in ZIO itself. | |
// Apart from all these, everything else should work straight away! | |
sealed trait Alg[_] | |
object Alg { | |
type Action[A] = Free[Alg, A] | |
case object Action1 extends Alg[Int] | |
case object Action2 extends Alg[Int] | |
case object ChildAction extends Alg[Int] | |
case object NonImportant extends Alg[Int] | |
def action1: Action[Int] = Action1.asAction | |
def action2: Action[Int] = Action2.asAction | |
def childAction: Action[Int] = ChildAction.asAction | |
def nonImp: Action[Int] = NonImportant.asAction | |
} | |
abstract sealed class StepInfo | |
object StepInfo { | |
final case class Info(parentResourceName: String, action: Action) extends StepInfo | |
final case object NoInfo extends StepInfo | |
} | |
abstract sealed class Action | |
object Action { | |
case object Create extends Action | |
case object Update extends Action | |
case object Delete extends Action | |
} | |
abstract sealed class Status | |
object Status { | |
case object Failed extends Status | |
case object Success extends Status | |
} | |
final case class AzureErr(reason: String) | |
final case class StepResult(resourceName: String, action: Action, status: Status) | |
trait InterpreterSyntax extends InterpreterTypes { | |
import StepInfo._ | |
implicit class ZIOSynstax[E, A](val f: Kleisli[IO[Nothing, ?], String, Either[E, A]]) { | |
def ~(msg: StepInfo): ActionWithLogT[E, A] = | |
msg match { | |
case Info(a, b) => EitherT[ActionWithLog, E, A] { | |
WriterT[ZIO, List[StepResult], Either[E, A]](f.map { | |
case Right(aa) => (List(StepResult(a, b, Status.Success)), Right(aa)) | |
case Left(aa) => (List(StepResult(a, b, Status.Failed)), Left(aa)) | |
}) | |
} | |
case NoInfo => EitherT[ActionWithLog, E, A] { | |
WriterT.liftF[ZIO, List[StepResult], Either[E, A]](f) | |
} | |
} | |
} | |
} | |
trait InterpreterTypes { | |
// A computation that can never crash. | |
type ZIO[A] = Kleisli[IO[Nothing, ?], String, A] | |
object ZIO { | |
def withClient[A](f: String => A) = ZioAttempt(f) | |
def withClientIO[A](f: String => IO[AzureErr, A]): ZIO[Either[AzureErr, A]] = { | |
Kleisli[IO[Nothing, ?], String, Either[AzureErr, A]](s => f(s).attempt) | |
} | |
final case class ZioAttempt[A]( f: String => A) { | |
def onError(context: => String): ZIO[Either[AzureErr, A]] = { | |
Kleisli[IO[Nothing, ?], String, Either[AzureErr, A]]( | |
a => | |
IO.syncException(f(a)).leftMap(t => AzureErr(context + ":" + t.getMessage)).attempt | |
) | |
} | |
} | |
} | |
// We use WriterT to accumulate the result of execution steps to be passed back to the user. | |
// Pls note, this approach is not for sys-log. If you are looking for actual logging, | |
// it's better off not to delay it using WriterTs and use one of the dozens of approaches available. | |
type ActionWithLog[A] = WriterT[ZIO, List[StepResult], A] | |
type ActionWithLogT[E, A] = EitherT[ActionWithLog, E, A] | |
} | |
trait Interpreter extends InterpreterSyntax { | |
import Alg._ | |
val interpreter: Alg ~> ActionWithLogT[AzureErr, ?] = new (Alg ~> ActionWithLogT[AzureErr, ?]) { | |
override def apply[A](fa: Alg[A]): ActionWithLogT[AzureErr, A] = (fa match { | |
case Action1 => | |
ZIO.withClient(_ => 1).onError("Failed when trying to fetch 1") ~ StepInfo.Info("azureresource", Action.Create) | |
case Action2 => | |
ZIO.withClientIO(_ => IO.syncException(throw new Exception("failed reason is this")).leftMap(t => AzureErr(t.getMessage))) ~ StepInfo.Info("secondone", Action.Create) | |
case ChildAction => | |
ZIO.withClient(_ => 2).onError( "Failed when trying to fetch 2") ~ StepInfo.Info("anotherone", Action.Update) | |
case NonImportant => | |
ZIO.withClient(_ => 3).onError("Failed when trying to fetch 4") ~ StepInfo.NoInfo | |
}).asInstanceOf[ActionWithLogT[AzureErr, A]] | |
} | |
} | |
object Main extends App with Interpreter { | |
import Alg._ | |
/** | |
* Free algebra version. Looks nicer ! It looks horrible in a finally tagless if we are composing this | |
* as we are early with eithert's and writert's and business logic interpreter level. | |
* In finally tagless, it would mostly be | |
* {{{ | |
* // ||> lifts to WriterTs. | |
* _ <- EitherT { 4.asRight[Throwable] ||> "Getting 4" } | |
* _ <- EitherT { 5.asRight[Throwable] ||> "Getting 5" } | |
* _ <- EitherT { new Exception("Failed 1").asLeft[Int] ||> "Getting 6" } | |
* _ <- EitherT { new Exception("Failed 2").asLeft[Int] ||> "Getting 7" } | |
* }}} | |
*/ | |
val comp: Free[Alg, Unit] = | |
for { | |
_ <- action1 | |
_ <- action1 | |
_ <- action1 | |
_ <- action1 | |
_ <- action1 | |
_ <- action1 | |
_ <- action1 | |
_ <- action1 | |
_ <- action1 | |
_ <- action1 | |
_ <- childAction | |
_ <- childAction | |
_ <- childAction | |
_ <- childAction | |
_ <- nonImp | |
_ <- action2 | |
_ <- action2 | |
_ <- action2 | |
_ <- action2 | |
} yield () | |
val res = comp.foldMap(interpreter).value.run.run("").asIO.unsafeRunSync() | |
println(res._1.mkString("\n")) | |
println(res._2) | |
// StepResult(azureresource,Create,Success) | |
// StepResult(azureresource,Create,Success) | |
// StepResult(azureresource,Create,Success) | |
// StepResult(azureresource,Create,Success) | |
// StepResult(azureresource,Create,Success) | |
// StepResult(azureresource,Create,Success) | |
// StepResult(azureresource,Create,Success) | |
// StepResult(azureresource,Create,Success) | |
// StepResult(azureresource,Create,Success) | |
// StepResult(azureresource,Create,Success) | |
// StepResult(anotherone,Update,Success) | |
// StepResult(anotherone,Update,Success) | |
// StepResult(anotherone,Update,Success) | |
// StepResult(anotherone,Update,Success) | |
// StepResult(secondone,Create,Failed) | |
// Left(AzureErr(failed reason is this)) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment