Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save afsalthaj/b5232ca198e27c369e899c2d83b6de2e to your computer and use it in GitHub Desktop.
Save afsalthaj/b5232ca198e27c369e899c2d83b6de2e to your computer and use it in GitHub Desktop.
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