From 1a35be29b24bc18c68b79375409635a0f827b13e Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Mon, 24 Jul 2023 12:43:55 -0400 Subject: [PATCH 1/9] Add signature for combine --- .../scala/zio/prelude/fx/ImperativeDsl.scala | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala diff --git a/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala b/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala new file mode 100644 index 000000000..91daead79 --- /dev/null +++ b/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala @@ -0,0 +1,131 @@ +package zio.prelude.fx + +import scala.annotation.tailrec +import ImperativeDsl._ + +sealed trait ImperativeDsl[Op[+_, +_], +E, +A] { self => + + final def catchAll[E2, A1 >: A]( + f: E => ImperativeDsl[Op, E2, A1] + ): ImperativeDsl[Op, E2, A1] = self match { + case free @ Sequence(fa, onSuccess, onOpailure) => + Sequence( + fa, + (a: free.InSuccess) => onSuccess(a).catchAll(f), + (e: free.InOpailure) => onOpailure(e).catchAll(f) + ) + case _ => ImperativeDsl.Sequence[Op, E, E2, A, A1](self, ImperativeDsl.Succeed(_), f) + } + + final def flatMap[E1 >: E, B](f: A => ImperativeDsl[Op, E1, B]): ImperativeDsl[Op, E1, B] = self match { + case free @ Sequence(fa, onSuccess, onOpailure) => + Sequence(fa, (a: free.InSuccess) => onSuccess(a).flatMap(f), (e: free.InOpailure) => onOpailure(e).flatMap(f)) + case _ => ImperativeDsl.Sequence[Op, E, E1, A, B](self, f, ImperativeDsl.Opail(_)) + } + + final def flatten[E1 >: E, B](implicit ev: A <:< ImperativeDsl[Op, E1, B]): ImperativeDsl[Op, E1, B] = + self.flatMap(ev) + + def interpret[G[+_, +_]]( + interpreter: ImperativeDsl.Interpreter[Op, G] + )(implicit g: ImperativeDsl.Executable[G]): G[E, A] = self match { + case ImperativeDsl.Succeed(a) => g.succeed(a) + case ImperativeDsl.Opail(e) => g.fail(e) + case ImperativeDsl.Eval(fa) => interpreter.interpret(fa) + case free @ ImperativeDsl.Sequence(fa, onSuccess, onOpailure) => + g.sequence( + fa.interpret(interpreter), + (a: free.InSuccess) => onSuccess(a).interpret(interpreter), + (e: free.InOpailure) => onOpailure(e).interpret(interpreter) + ) + } + + final def map[B](f: A => B): ImperativeDsl[Op, E, B] = + self.flatMap(a => ImperativeDsl.Succeed(f(a))) + + final def mapError[E2](f: E => E2): ImperativeDsl[Op, E2, A] = + self.catchAll(e => ImperativeDsl.Opail(f(e))) + + def unsafeInterpret( + unsafeInterpreter: ImperativeDsl.UnsafeInterpreter[Op] + ): Either[E, A] = { + @tailrec + def loop( + free: ImperativeDsl[Op, Any, Any], + stack: List[ImperativeDsl.Sequence[Op, Any, Any, Any, Any]] + ): Either[E, A] = + free match { + case ImperativeDsl.Succeed(a) => + stack match { + case ImperativeDsl.Sequence(_, onSuccess, _) :: stack => loop(onSuccess(a), stack) + case Nil => Right(a.asInstanceOf[A]) + } + case ImperativeDsl.Opail(e) => + stack match { + case ImperativeDsl.Sequence(_, _, onOpailure) :: stack => loop(onOpailure(e), stack) + case Nil => Left(e.asInstanceOf[E]) + } + case ImperativeDsl.Eval(fa) => + unsafeInterpreter.interpret(fa) match { + case Left(e) => + stack match { + case ImperativeDsl.Sequence(_, _, onOpailure) :: stack => loop(onOpailure(e), stack) + case Nil => Left(e.asInstanceOf[E]) + } + case Right(a) => + stack match { + case ImperativeDsl.Sequence(_, onSuccess, _) :: stack => loop(onSuccess(a), stack) + case Nil => Right(a.asInstanceOf[A]) + } + } + case free @ ImperativeDsl.Sequence(fa, onSuccess, onOpailure) => + loop(fa, (free :: stack).asInstanceOf[List[ImperativeDsl.Sequence[Op, Any, Any, Any, Any]]]) + } + loop(self, Nil) + } +} + +object ImperativeDsl { + def eval[Op[+_, +_], E, A](fa: Op[E, A]): ImperativeDsl[Op, E, A] = Eval(fa) + def fail[Op[+_, +_], E](e: E): ImperativeDsl[Op, E, Nothing] = Opail(e) + def succeed[Op[+_, +_], A](a: A): ImperativeDsl[Op, Nothing, A] = Succeed(a) + + final case class Succeed[Op[+_, +_], A](a: A) extends ImperativeDsl[Op, Nothing, A] + final case class Opail[Op[+_, +_], E](a: E) extends ImperativeDsl[Op, E, Nothing] + final case class Eval[Op[+_, +_], E, A](fa: Op[E, A]) extends ImperativeDsl[Op, E, A] + final case class Sequence[Op[+_, +_], E1, E2, A1, A2] private[ImperativeDsl] ( + fa: ImperativeDsl[Op, E1, A1], + onSuccess: A1 => ImperativeDsl[Op, E2, A2], + onOpailure: E1 => ImperativeDsl[Op, E2, A2] + ) extends ImperativeDsl[Op, E2, A2] { + type InSuccess = A1 + type InOpailure = E1 + } + + // TODO: Consider renaming G to Dsl/Executable + trait Interpreter[Op[+_, +_], G[+_, +_]] { + def interpret[E, A](fa: Op[E, A]): G[E, A] + + def combine[Op2[+_, +_]]( + that: Interpreter[Op2, G] + ): Interpreter[({ type lambda[+E, +A] = CompositeOp[Op, Op2, E, A] })#lambda, G] = ??? + } + + trait UnsafeInterpreter[Op[+_, +_]] { + def interpret[E, A](fa: Op[E, A]): Either[E, A] + } + + trait Executable[Op[+_, +_]] { + def succeed[A](a: A): Op[Nothing, A] + def fail[E](e: E): Op[E, Nothing] + def eval[E, A](fa: Op[E, A]): Op[E, A] + def sequence[E1, E2, A1, A2]( + fa: Op[E1, A1], + onSuccess: A1 => Op[E2, A2], + onFailure: E1 => Op[E2, A2] + ): Op[E2, A2] + } + + // TODO: Consider Making: CompositeOp[+Op1[+_, +_], +Op2[+_, +_], +E, +A] + final case class CompositeOp[Op1[+_, +_], Op2[+_, +_], +E, +A](run: Either[Op1[E, A], Op2[E, A]]) { self => } +} From f57660edd8c0538db8662d282b8498bae80a7f75 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Thu, 27 Jul 2023 09:11:59 -0400 Subject: [PATCH 2/9] Working through ImperativeDsl. Added a ZPureExecutable and added a test --- .../zio/prelude/fx/ImperativeDslSpec.scala | 100 ++++++++++++++++++ .../scala/zio/prelude/fx/ImperativeDsl.scala | 43 ++++++-- 2 files changed, 135 insertions(+), 8 deletions(-) create mode 100644 core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeDslSpec.scala diff --git a/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeDslSpec.scala b/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeDslSpec.scala new file mode 100644 index 000000000..ee01c3d45 --- /dev/null +++ b/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeDslSpec.scala @@ -0,0 +1,100 @@ +package zio.prelude.fx + +import zio.prelude._ +import zio.test._ +import ImperativeDslSpec.transitSystem.{ops, dsl} +import ops.Card +import scala.language.reflectiveCalls + +object ImperativeDslSpec extends ZIOBaseSpec { + def spec: Spec[Environment, Any] = suite("ImperativeDslSpec")( + suite("unsafeInterpret")( + test("Interpreting a getRiderCount after 2 authorized riders") { + val john = new { + val ridesCard = Card.TransitRideCard(2) + } + + val jane = new { + val debitCard = Card.DebitCard(10_00) + } + + val interpreter = transitSystem.interpreters.default(farePriceInCents = 2_50) + + val program = for { + _ <- dsl.authorize(john.ridesCard) + _ <- dsl.authorize(jane.debitCard) + c <- dsl.getRiderCount + } yield c + + val result = program.interpret(interpreter) + val actual = result.runEither + + assertTrue(actual == Right(2)) + } + ) + ) + + object transitSystem { + object ops { + sealed trait TransitSystemOp[+E, +A] extends Product with Serializable + final case class Authorize(card: Card) extends TransitSystemOp[AccessDeniedError, Card] + case object GetRiderCount extends TransitSystemOp[Nothing, Int] + + sealed trait Card + object Card { + final case class DebitCard(balance: Int) extends Card + final case class TransitRideCard(rides: Int) extends Card + } + + sealed trait TransitSystemError + sealed trait AccessDeniedError extends TransitSystemError + object TransitSystemError { + final case object InsufficientBalance extends AccessDeniedError + final case object NoRides extends AccessDeniedError + } + } + + object dsl { + import ops._ + + type TSys[+E, +A] = ImperativeDsl[TransitSystemOp, E, A] + + def authorize(card: Card): TSys[AccessDeniedError, Card] = + ImperativeDsl.eval(Authorize(card)) + + def getRiderCount: TSys[Nothing, Int] = ImperativeDsl.eval(GetRiderCount) + } + + object interpreters { + import ImperativeDsl.Interpreter + import ops._ + type Result[+E, +A] = zio.prelude.fx.ZPure[String, Unit, Unit, Any, E, A] + def default(farePriceInCents: Int, initialRiderCount: Int = 0): Interpreter[TransitSystemOp, Result] = { + var riderCount = initialRiderCount + new Interpreter[TransitSystemOp, Result] { + override def interpret[E, A](fa: TransitSystemOp[E, A]): Result[E, A] = + fa match { + case Authorize(card) => + card match { + case Card.DebitCard(balance) => + if (balance >= farePriceInCents) { + riderCount += 1 + ZPure.succeed(Card.DebitCard(balance - farePriceInCents)) + } else { + ZPure.fail(TransitSystemError.InsufficientBalance) + } + case Card.TransitRideCard(rides) => + if (rides > 0) { + riderCount += 1 + ZPure.succeed(Card.TransitRideCard(rides - 1)) + } else { + ZPure.fail(TransitSystemError.NoRides) + } + } + case GetRiderCount => ZPure.succeed(riderCount) + } + } + } + } + } +} diff --git a/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala b/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala index 91daead79..2e736433d 100644 --- a/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala +++ b/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala @@ -115,17 +115,44 @@ object ImperativeDsl { def interpret[E, A](fa: Op[E, A]): Either[E, A] } - trait Executable[Op[+_, +_]] { - def succeed[A](a: A): Op[Nothing, A] - def fail[E](e: E): Op[E, Nothing] - def eval[E, A](fa: Op[E, A]): Op[E, A] + trait Executable[F[+_, +_]] { + def succeed[A](a: A): F[Nothing, A] + def fail[E](e: E): F[E, Nothing] + def eval[E, A](fa: F[E, A]): F[E, A] def sequence[E1, E2, A1, A2]( - fa: Op[E1, A1], - onSuccess: A1 => Op[E2, A2], - onFailure: E1 => Op[E2, A2] - ): Op[E2, A2] + fa: F[E1, A1], + onSuccess: A1 => F[E2, A2], + onFailure: E1 => F[E2, A2] + ): F[E2, A2] } // TODO: Consider Making: CompositeOp[+Op1[+_, +_], +Op2[+_, +_], +E, +A] final case class CompositeOp[Op1[+_, +_], Op2[+_, +_], +E, +A](run: Either[Op1[E, A], Op2[E, A]]) { self => } + + // TODO: Consider what can be done to make the type lambda here simpler + implicit def ZPureExecutable[W]: Executable[({ type lambda[+E, +A] = ZPure[W, Unit, Unit, Any, E, A] })#lambda] = { + // ({ type lambda[+E, +A] = ZPure[W, Unit, Unit, Any, E, A] })#lambda + type Result[+E, +A] = ZPure[W, Unit, Unit, Any, E, A] + val Result: zio.prelude.fx.ZPure.type = zio.prelude.fx.ZPure + new Executable[Result] { + + override def succeed[A](a: A): Result[Nothing, A] = Result.succeed(a) + + override def fail[E](e: E): Result[E, Nothing] = Result.fail(e) + + override def eval[E, A](fa: Result[E, A]): Result[E, A] = Result.suspend(fa) + + override def sequence[E1, E2, A1, A2]( + fa: Result[E1, A1], + onSuccess: A1 => Result[E2, A2], + onFailure: E1 => Result[E2, A2] + ): Result[E2, A2] = Result.suspend { + // TODO: Consider if this can be done with foldM since its pissible E1 or E2 is Nothing + fa.foldM( + onFailure, + onSuccess + ) + } + } + } } From 3a7a568d2fa1c97a1d0b93bde851ff813859eb1d Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:05:18 -0400 Subject: [PATCH 3/9] Rename some things to provide "hopefully" more approachable naming --- .../zio/prelude/fx/ImperativeDslSpec.scala | 37 ++--- .../scala/zio/prelude/fx/ImperativeDsl.scala | 129 +++++++++++------- 2 files changed, 95 insertions(+), 71 deletions(-) diff --git a/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeDslSpec.scala b/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeDslSpec.scala index ee01c3d45..37ab51912 100644 --- a/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeDslSpec.scala +++ b/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeDslSpec.scala @@ -2,14 +2,15 @@ package zio.prelude.fx import zio.prelude._ import zio.test._ -import ImperativeDslSpec.transitSystem.{ops, dsl} -import ops.Card +import ImperativeDslSpec.transitSystem.{Dsl, syntax} +import Dsl.Card import scala.language.reflectiveCalls object ImperativeDslSpec extends ZIOBaseSpec { def spec: Spec[Environment, Any] = suite("ImperativeDslSpec")( suite("unsafeInterpret")( test("Interpreting a getRiderCount after 2 authorized riders") { + import syntax._ val john = new { val ridesCard = Card.TransitRideCard(2) } @@ -21,10 +22,10 @@ object ImperativeDslSpec extends ZIOBaseSpec { val interpreter = transitSystem.interpreters.default(farePriceInCents = 2_50) val program = for { - _ <- dsl.authorize(john.ridesCard) - _ <- dsl.authorize(jane.debitCard) - c <- dsl.getRiderCount - } yield c + _ <- authorize(john.ridesCard) + _ <- authorize(jane.debitCard) + cnt <- getRiderCount + } yield cnt val result = program.interpret(interpreter) val actual = result.runEither @@ -35,10 +36,10 @@ object ImperativeDslSpec extends ZIOBaseSpec { ) object transitSystem { - object ops { - sealed trait TransitSystemOp[+E, +A] extends Product with Serializable - final case class Authorize(card: Card) extends TransitSystemOp[AccessDeniedError, Card] - case object GetRiderCount extends TransitSystemOp[Nothing, Int] + object Dsl { + sealed trait TransitSystemDsl[+E, +A] extends Product with Serializable + final case class Authorize(card: Card) extends TransitSystemDsl[AccessDeniedError, Card] + case object GetRiderCount extends TransitSystemDsl[Nothing, Int] sealed trait Card object Card { @@ -54,10 +55,10 @@ object ImperativeDslSpec extends ZIOBaseSpec { } } - object dsl { - import ops._ + object syntax { + import Dsl._ - type TSys[+E, +A] = ImperativeDsl[TransitSystemOp, E, A] + type TSys[+E, +A] = ImperativeDsl[TransitSystemDsl, E, A] def authorize(card: Card): TSys[AccessDeniedError, Card] = ImperativeDsl.eval(Authorize(card)) @@ -67,13 +68,13 @@ object ImperativeDslSpec extends ZIOBaseSpec { object interpreters { import ImperativeDsl.Interpreter - import ops._ + import Dsl._ type Result[+E, +A] = zio.prelude.fx.ZPure[String, Unit, Unit, Any, E, A] - def default(farePriceInCents: Int, initialRiderCount: Int = 0): Interpreter[TransitSystemOp, Result] = { + def default(farePriceInCents: Int, initialRiderCount: Int = 0): Interpreter[TransitSystemDsl, Result] = { var riderCount = initialRiderCount - new Interpreter[TransitSystemOp, Result] { - override def interpret[E, A](fa: TransitSystemOp[E, A]): Result[E, A] = - fa match { + new Interpreter[TransitSystemDsl, Result] { + override def interpret[E, A](dsl: TransitSystemDsl[E, A]): Result[E, A] = + dsl match { case Authorize(card) => card match { case Card.DebitCard(balance) => diff --git a/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala b/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala index 2e736433d..b2cf52bd8 100644 --- a/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala +++ b/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala @@ -3,56 +3,69 @@ package zio.prelude.fx import scala.annotation.tailrec import ImperativeDsl._ -sealed trait ImperativeDsl[Op[+_, +_], +E, +A] { self => +/** + * An `ImperativeDsl[Dsl, E, A]` is a data structure that provides the ability to execute a user provided DSL as a sequence of operations. + * From a theoretical standpoint `ImperativeDsl` is an implementation of a Free Monad. + * @tparam Dsl - the user's DSL + * @tparam E - the error type if any + * @tparam A - the result type + */ +sealed trait ImperativeDsl[Dsl[+_, +_], +E, +A] { self => final def catchAll[E2, A1 >: A]( - f: E => ImperativeDsl[Op, E2, A1] - ): ImperativeDsl[Op, E2, A1] = self match { - case free @ Sequence(fa, onSuccess, onOpailure) => + f: E => ImperativeDsl[Dsl, E2, A1] + ): ImperativeDsl[Dsl, E2, A1] = self match { + case free @ Sequence(fa, onSuccess, onFailure) => Sequence( fa, (a: free.InSuccess) => onSuccess(a).catchAll(f), - (e: free.InOpailure) => onOpailure(e).catchAll(f) + (e: free.InFailure) => onFailure(e).catchAll(f) ) - case _ => ImperativeDsl.Sequence[Op, E, E2, A, A1](self, ImperativeDsl.Succeed(_), f) + case _ => ImperativeDsl.Sequence[Dsl, E, E2, A, A1](self, ImperativeDsl.Succeed(_), f) } - final def flatMap[E1 >: E, B](f: A => ImperativeDsl[Op, E1, B]): ImperativeDsl[Op, E1, B] = self match { + final def flatMap[E1 >: E, B](f: A => ImperativeDsl[Dsl, E1, B]): ImperativeDsl[Dsl, E1, B] = self match { case free @ Sequence(fa, onSuccess, onOpailure) => - Sequence(fa, (a: free.InSuccess) => onSuccess(a).flatMap(f), (e: free.InOpailure) => onOpailure(e).flatMap(f)) - case _ => ImperativeDsl.Sequence[Op, E, E1, A, B](self, f, ImperativeDsl.Opail(_)) + Sequence( + fa, + (a: free.InSuccess) => + onSuccess(a) + .flatMap(f), + (e: free.InFailure) => onOpailure(e).flatMap(f) + ) + case _ => ImperativeDsl.Sequence[Dsl, E, E1, A, B](self, f, ImperativeDsl.Opail(_)) } - final def flatten[E1 >: E, B](implicit ev: A <:< ImperativeDsl[Op, E1, B]): ImperativeDsl[Op, E1, B] = + final def flatten[E1 >: E, B](implicit ev: A <:< ImperativeDsl[Dsl, E1, B]): ImperativeDsl[Dsl, E1, B] = self.flatMap(ev) - def interpret[G[+_, +_]]( - interpreter: ImperativeDsl.Interpreter[Op, G] - )(implicit g: ImperativeDsl.Executable[G]): G[E, A] = self match { - case ImperativeDsl.Succeed(a) => g.succeed(a) - case ImperativeDsl.Opail(e) => g.fail(e) - case ImperativeDsl.Eval(fa) => interpreter.interpret(fa) - case free @ ImperativeDsl.Sequence(fa, onSuccess, onOpailure) => - g.sequence( + def interpret[Executable[+_, +_]]( + interpreter: ImperativeDsl.Interpreter[Dsl, Executable] + )(implicit exe: ImperativeDsl.ToExecutable[Executable]): Executable[E, A] = self match { + case ImperativeDsl.Succeed(a) => exe.succeed(a) + case ImperativeDsl.Opail(e) => exe.fail(e) + case ImperativeDsl.Eval(fa) => interpreter.interpret(fa) + case free @ ImperativeDsl.Sequence(fa, onSuccess, onFailure) => + exe.sequence( fa.interpret(interpreter), (a: free.InSuccess) => onSuccess(a).interpret(interpreter), - (e: free.InOpailure) => onOpailure(e).interpret(interpreter) + (e: free.InFailure) => onFailure(e).interpret(interpreter) ) } - final def map[B](f: A => B): ImperativeDsl[Op, E, B] = + final def map[B](f: A => B): ImperativeDsl[Dsl, E, B] = self.flatMap(a => ImperativeDsl.Succeed(f(a))) - final def mapError[E2](f: E => E2): ImperativeDsl[Op, E2, A] = + final def mapError[E2](f: E => E2): ImperativeDsl[Dsl, E2, A] = self.catchAll(e => ImperativeDsl.Opail(f(e))) def unsafeInterpret( - unsafeInterpreter: ImperativeDsl.UnsafeInterpreter[Op] + unsafeInterpreter: ImperativeDsl.UnsafeInterpreter[Dsl] ): Either[E, A] = { @tailrec def loop( - free: ImperativeDsl[Op, Any, Any], - stack: List[ImperativeDsl.Sequence[Op, Any, Any, Any, Any]] + free: ImperativeDsl[Dsl, Any, Any], + stack: List[ImperativeDsl.Sequence[Dsl, Any, Any, Any, Any]] ): Either[E, A] = free match { case ImperativeDsl.Succeed(a) => @@ -62,15 +75,15 @@ sealed trait ImperativeDsl[Op[+_, +_], +E, +A] { self => } case ImperativeDsl.Opail(e) => stack match { - case ImperativeDsl.Sequence(_, _, onOpailure) :: stack => loop(onOpailure(e), stack) - case Nil => Left(e.asInstanceOf[E]) + case ImperativeDsl.Sequence(_, _, onFailure) :: stack => loop(onFailure(e), stack) + case Nil => Left(e.asInstanceOf[E]) } case ImperativeDsl.Eval(fa) => unsafeInterpreter.interpret(fa) match { case Left(e) => stack match { - case ImperativeDsl.Sequence(_, _, onOpailure) :: stack => loop(onOpailure(e), stack) - case Nil => Left(e.asInstanceOf[E]) + case ImperativeDsl.Sequence(_, _, onFailure) :: stack => loop(onFailure(e), stack) + case Nil => Left(e.asInstanceOf[E]) } case Right(a) => stack match { @@ -79,7 +92,7 @@ sealed trait ImperativeDsl[Op[+_, +_], +E, +A] { self => } } case free @ ImperativeDsl.Sequence(fa, onSuccess, onOpailure) => - loop(fa, (free :: stack).asInstanceOf[List[ImperativeDsl.Sequence[Op, Any, Any, Any, Any]]]) + loop(fa, (free :: stack).asInstanceOf[List[ImperativeDsl.Sequence[Dsl, Any, Any, Any, Any]]]) } loop(self, Nil) } @@ -96,45 +109,55 @@ object ImperativeDsl { final case class Sequence[Op[+_, +_], E1, E2, A1, A2] private[ImperativeDsl] ( fa: ImperativeDsl[Op, E1, A1], onSuccess: A1 => ImperativeDsl[Op, E2, A2], - onOpailure: E1 => ImperativeDsl[Op, E2, A2] + onFailure: E1 => ImperativeDsl[Op, E2, A2] ) extends ImperativeDsl[Op, E2, A2] { - type InSuccess = A1 - type InOpailure = E1 + type InSuccess = A1 + type InFailure = E1 } - // TODO: Consider renaming G to Dsl/Executable - trait Interpreter[Op[+_, +_], G[+_, +_]] { - def interpret[E, A](fa: Op[E, A]): G[E, A] - - def combine[Op2[+_, +_]]( - that: Interpreter[Op2, G] - ): Interpreter[({ type lambda[+E, +A] = CompositeOp[Op, Op2, E, A] })#lambda, G] = ??? + /// Interpreter provides the ability to interpret a DSL into an executable program + trait Interpreter[Dsl[+_, +_], Executable[+_, +_]] { self => + def interpret[E, A](dsl: Dsl[E, A]): Executable[E, A] + + def combine[Dsl2[+_, +_]]( + that: Interpreter[Dsl2, Executable] + ): Interpreter[({ type lambda[+E, +A] = CompositeDsl[Dsl, Dsl2, E, A] })#lambda, Executable] = + new Interpreter[({ type lambda[+E, +A] = CompositeDsl[Dsl, Dsl2, E, A] })#lambda, Executable] { + override def interpret[E, A](dsl: CompositeDsl[Dsl, Dsl2, E, A]): Executable[E, A] = dsl.eitherDsl match { + case Left(dsl) => self.interpret(dsl) + case Right(dsl) => that.interpret(dsl) + } + } } - trait UnsafeInterpreter[Op[+_, +_]] { - def interpret[E, A](fa: Op[E, A]): Either[E, A] + trait UnsafeInterpreter[Dsl[+_, +_]] { + def interpret[E, A](fa: Dsl[E, A]): Either[E, A] } - trait Executable[F[+_, +_]] { - def succeed[A](a: A): F[Nothing, A] - def fail[E](e: E): F[E, Nothing] - def eval[E, A](fa: F[E, A]): F[E, A] + trait ToExecutable[Executable[+_, +_]] { + def succeed[A](a: A): Executable[Nothing, A] + def fail[E](e: E): Executable[E, Nothing] + def eval[E, A](fa: Executable[E, A]): Executable[E, A] def sequence[E1, E2, A1, A2]( - fa: F[E1, A1], - onSuccess: A1 => F[E2, A2], - onFailure: E1 => F[E2, A2] - ): F[E2, A2] + fa: Executable[E1, A1], + onSuccess: A1 => Executable[E2, A2], + onFailure: E1 => Executable[E2, A2] + ): Executable[E2, A2] } - // TODO: Consider Making: CompositeOp[+Op1[+_, +_], +Op2[+_, +_], +E, +A] - final case class CompositeOp[Op1[+_, +_], Op2[+_, +_], +E, +A](run: Either[Op1[E, A], Op2[E, A]]) { self => } + final case class CompositeDsl[+Dsl1[+_, +_], +Dsl2[+_, +_], +E, +A](eitherDsl: Either[Dsl1[E, A], Dsl2[E, A]]) + extends AnyVal { self => + type InSuccess <: A + type InFailure <: E + } // TODO: Consider what can be done to make the type lambda here simpler - implicit def ZPureExecutable[W]: Executable[({ type lambda[+E, +A] = ZPure[W, Unit, Unit, Any, E, A] })#lambda] = { + implicit def ZPureToExecutable[W] + : ToExecutable[({ type lambda[+E, +A] = ZPure[W, Unit, Unit, Any, E, A] })#lambda] = { // ({ type lambda[+E, +A] = ZPure[W, Unit, Unit, Any, E, A] })#lambda type Result[+E, +A] = ZPure[W, Unit, Unit, Any, E, A] val Result: zio.prelude.fx.ZPure.type = zio.prelude.fx.ZPure - new Executable[Result] { + new ToExecutable[Result] { override def succeed[A](a: A): Result[Nothing, A] = Result.succeed(a) From a441a8818323564092283c4da815428fe448e874 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:36:23 -0400 Subject: [PATCH 4/9] Fix up tests --- .../scala/zio/prelude/fx/ImperativeDslSpec.scala | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeDslSpec.scala b/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeDslSpec.scala index 37ab51912..3114f81b6 100644 --- a/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeDslSpec.scala +++ b/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeDslSpec.scala @@ -11,19 +11,16 @@ object ImperativeDslSpec extends ZIOBaseSpec { suite("unsafeInterpret")( test("Interpreting a getRiderCount after 2 authorized riders") { import syntax._ - val john = new { - val ridesCard = Card.TransitRideCard(2) - } + case class Customer(name:String, card:Card) - val jane = new { - val debitCard = Card.DebitCard(10_00) - } + val john = Customer("John", Card.TransitRideCard(2)) + val jane = Customer("Jane", Card.DebitCard(10_00)) val interpreter = transitSystem.interpreters.default(farePriceInCents = 2_50) val program = for { - _ <- authorize(john.ridesCard) - _ <- authorize(jane.debitCard) + _ <- authorize(john.card) + _ <- authorize(jane.card) cnt <- getRiderCount } yield cnt From 60516845ddae348918a3e639aa2aafc7b092a127 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Fri, 28 Jul 2023 07:34:38 -0400 Subject: [PATCH 5/9] Fix number formatting with _ not supported in Scala 2.11 as well as unused imports and variables --- .../scala/zio/prelude/fx/ImperativeDslSpec.scala | 5 ++--- .../scala/zio/prelude/fx/ImperativeDsl.scala | 16 ++++++++-------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeDslSpec.scala b/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeDslSpec.scala index 3114f81b6..5ba31ac54 100644 --- a/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeDslSpec.scala +++ b/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeDslSpec.scala @@ -4,7 +4,6 @@ import zio.prelude._ import zio.test._ import ImperativeDslSpec.transitSystem.{Dsl, syntax} import Dsl.Card -import scala.language.reflectiveCalls object ImperativeDslSpec extends ZIOBaseSpec { def spec: Spec[Environment, Any] = suite("ImperativeDslSpec")( @@ -14,9 +13,9 @@ object ImperativeDslSpec extends ZIOBaseSpec { case class Customer(name:String, card:Card) val john = Customer("John", Card.TransitRideCard(2)) - val jane = Customer("Jane", Card.DebitCard(10_00)) + val jane = Customer("Jane", Card.DebitCard(1000)) - val interpreter = transitSystem.interpreters.default(farePriceInCents = 2_50) + val interpreter = transitSystem.interpreters.default(farePriceInCents = 250) val program = for { _ <- authorize(john.card) diff --git a/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala b/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala index b2cf52bd8..3b32a4573 100644 --- a/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala +++ b/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala @@ -5,7 +5,7 @@ import ImperativeDsl._ /** * An `ImperativeDsl[Dsl, E, A]` is a data structure that provides the ability to execute a user provided DSL as a sequence of operations. - * From a theoretical standpoint `ImperativeDsl` is an implementation of a Free Monad. + * From a theoretical standpoint `ImperativeDsl` is an implementation of a Free Monad.`` * @tparam Dsl - the user's DSL * @tparam E - the error type if any * @tparam A - the result type @@ -25,15 +25,15 @@ sealed trait ImperativeDsl[Dsl[+_, +_], +E, +A] { self => } final def flatMap[E1 >: E, B](f: A => ImperativeDsl[Dsl, E1, B]): ImperativeDsl[Dsl, E1, B] = self match { - case free @ Sequence(fa, onSuccess, onOpailure) => + case free @ Sequence(fa, onSuccess, onFailure) => Sequence( fa, (a: free.InSuccess) => onSuccess(a) .flatMap(f), - (e: free.InFailure) => onOpailure(e).flatMap(f) + (e: free.InFailure) => onFailure(e).flatMap(f) ) - case _ => ImperativeDsl.Sequence[Dsl, E, E1, A, B](self, f, ImperativeDsl.Opail(_)) + case _ => ImperativeDsl.Sequence[Dsl, E, E1, A, B](self, f, ImperativeDsl.Opail(_)) } final def flatten[E1 >: E, B](implicit ev: A <:< ImperativeDsl[Dsl, E1, B]): ImperativeDsl[Dsl, E1, B] = @@ -68,17 +68,17 @@ sealed trait ImperativeDsl[Dsl[+_, +_], +E, +A] { self => stack: List[ImperativeDsl.Sequence[Dsl, Any, Any, Any, Any]] ): Either[E, A] = free match { - case ImperativeDsl.Succeed(a) => + case ImperativeDsl.Succeed(a) => stack match { case ImperativeDsl.Sequence(_, onSuccess, _) :: stack => loop(onSuccess(a), stack) case Nil => Right(a.asInstanceOf[A]) } - case ImperativeDsl.Opail(e) => + case ImperativeDsl.Opail(e) => stack match { case ImperativeDsl.Sequence(_, _, onFailure) :: stack => loop(onFailure(e), stack) case Nil => Left(e.asInstanceOf[E]) } - case ImperativeDsl.Eval(fa) => + case ImperativeDsl.Eval(fa) => unsafeInterpreter.interpret(fa) match { case Left(e) => stack match { @@ -91,7 +91,7 @@ sealed trait ImperativeDsl[Dsl[+_, +_], +E, +A] { self => case Nil => Right(a.asInstanceOf[A]) } } - case free @ ImperativeDsl.Sequence(fa, onSuccess, onOpailure) => + case free @ ImperativeDsl.Sequence(fa, _, _) => loop(fa, (free :: stack).asInstanceOf[List[ImperativeDsl.Sequence[Dsl, Any, Any, Any, Any]]]) } loop(self, Nil) From 8990cd75f0a19d7710286e61705af8bf874f5022 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Fri, 28 Jul 2023 08:22:09 -0400 Subject: [PATCH 6/9] Update core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala Co-authored-by: Adam Fraser --- core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala b/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala index 3b32a4573..9933c42c6 100644 --- a/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala +++ b/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala @@ -104,7 +104,7 @@ object ImperativeDsl { def succeed[Op[+_, +_], A](a: A): ImperativeDsl[Op, Nothing, A] = Succeed(a) final case class Succeed[Op[+_, +_], A](a: A) extends ImperativeDsl[Op, Nothing, A] - final case class Opail[Op[+_, +_], E](a: E) extends ImperativeDsl[Op, E, Nothing] + final case class Fail[Op[+_, +_], E](a: E) extends ImperativeDsl[Op, E, Nothing] final case class Eval[Op[+_, +_], E, A](fa: Op[E, A]) extends ImperativeDsl[Op, E, A] final case class Sequence[Op[+_, +_], E1, E2, A1, A2] private[ImperativeDsl] ( fa: ImperativeDsl[Op, E1, A1], From 9a3f771ba47d3c3f94d83e8314be47ef509f2d5d Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Fri, 28 Jul 2023 09:28:58 -0400 Subject: [PATCH 7/9] Fix typos and use of Op --- .../scala/zio/prelude/fx/ImperativeDsl.scala | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala b/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala index 9933c42c6..c33bd7148 100644 --- a/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala +++ b/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala @@ -1,3 +1,18 @@ +/* + * Copyright 2020-2023 John A. De Goes and the ZIO Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package zio.prelude.fx import scala.annotation.tailrec @@ -6,9 +21,6 @@ import ImperativeDsl._ /** * An `ImperativeDsl[Dsl, E, A]` is a data structure that provides the ability to execute a user provided DSL as a sequence of operations. * From a theoretical standpoint `ImperativeDsl` is an implementation of a Free Monad.`` - * @tparam Dsl - the user's DSL - * @tparam E - the error type if any - * @tparam A - the result type */ sealed trait ImperativeDsl[Dsl[+_, +_], +E, +A] { self => @@ -33,7 +45,7 @@ sealed trait ImperativeDsl[Dsl[+_, +_], +E, +A] { self => .flatMap(f), (e: free.InFailure) => onFailure(e).flatMap(f) ) - case _ => ImperativeDsl.Sequence[Dsl, E, E1, A, B](self, f, ImperativeDsl.Opail(_)) + case _ => ImperativeDsl.Sequence[Dsl, E, E1, A, B](self, f, ImperativeDsl.Fail(_)) } final def flatten[E1 >: E, B](implicit ev: A <:< ImperativeDsl[Dsl, E1, B]): ImperativeDsl[Dsl, E1, B] = @@ -43,7 +55,7 @@ sealed trait ImperativeDsl[Dsl[+_, +_], +E, +A] { self => interpreter: ImperativeDsl.Interpreter[Dsl, Executable] )(implicit exe: ImperativeDsl.ToExecutable[Executable]): Executable[E, A] = self match { case ImperativeDsl.Succeed(a) => exe.succeed(a) - case ImperativeDsl.Opail(e) => exe.fail(e) + case ImperativeDsl.Fail(e) => exe.fail(e) case ImperativeDsl.Eval(fa) => interpreter.interpret(fa) case free @ ImperativeDsl.Sequence(fa, onSuccess, onFailure) => exe.sequence( @@ -57,7 +69,7 @@ sealed trait ImperativeDsl[Dsl[+_, +_], +E, +A] { self => self.flatMap(a => ImperativeDsl.Succeed(f(a))) final def mapError[E2](f: E => E2): ImperativeDsl[Dsl, E2, A] = - self.catchAll(e => ImperativeDsl.Opail(f(e))) + self.catchAll(e => ImperativeDsl.Fail(f(e))) def unsafeInterpret( unsafeInterpreter: ImperativeDsl.UnsafeInterpreter[Dsl] @@ -73,7 +85,7 @@ sealed trait ImperativeDsl[Dsl[+_, +_], +E, +A] { self => case ImperativeDsl.Sequence(_, onSuccess, _) :: stack => loop(onSuccess(a), stack) case Nil => Right(a.asInstanceOf[A]) } - case ImperativeDsl.Opail(e) => + case ImperativeDsl.Fail(e) => stack match { case ImperativeDsl.Sequence(_, _, onFailure) :: stack => loop(onFailure(e), stack) case Nil => Left(e.asInstanceOf[E]) @@ -99,18 +111,18 @@ sealed trait ImperativeDsl[Dsl[+_, +_], +E, +A] { self => } object ImperativeDsl { - def eval[Op[+_, +_], E, A](fa: Op[E, A]): ImperativeDsl[Op, E, A] = Eval(fa) - def fail[Op[+_, +_], E](e: E): ImperativeDsl[Op, E, Nothing] = Opail(e) - def succeed[Op[+_, +_], A](a: A): ImperativeDsl[Op, Nothing, A] = Succeed(a) - - final case class Succeed[Op[+_, +_], A](a: A) extends ImperativeDsl[Op, Nothing, A] - final case class Fail[Op[+_, +_], E](a: E) extends ImperativeDsl[Op, E, Nothing] - final case class Eval[Op[+_, +_], E, A](fa: Op[E, A]) extends ImperativeDsl[Op, E, A] - final case class Sequence[Op[+_, +_], E1, E2, A1, A2] private[ImperativeDsl] ( - fa: ImperativeDsl[Op, E1, A1], - onSuccess: A1 => ImperativeDsl[Op, E2, A2], - onFailure: E1 => ImperativeDsl[Op, E2, A2] - ) extends ImperativeDsl[Op, E2, A2] { + def eval[Dsl[+_, +_], E, A](fa: Dsl[E, A]): ImperativeDsl[Dsl, E, A] = Eval(fa) + def fail[Dsl[+_, +_], E](e: E): ImperativeDsl[Dsl, E, Nothing] = Fail(e) + def succeed[Dsl[+_, +_], A](a: A): ImperativeDsl[Dsl, Nothing, A] = Succeed(a) + + final case class Succeed[Dsl[+_, +_], A](a: A) extends ImperativeDsl[Dsl, Nothing, A] + final case class Fail[Dsl[+_, +_], E](a: E) extends ImperativeDsl[Dsl, E, Nothing] + final case class Eval[Dsl[+_, +_], E, A](fa: Dsl[E, A]) extends ImperativeDsl[Dsl, E, A] + final case class Sequence[Dsl[+_, +_], E1, E2, A1, A2] private[ImperativeDsl] ( + dsl: ImperativeDsl[Dsl, E1, A1], + onSuccess: A1 => ImperativeDsl[Dsl, E2, A2], + onFailure: E1 => ImperativeDsl[Dsl, E2, A2] + ) extends ImperativeDsl[Dsl, E2, A2] { type InSuccess = A1 type InFailure = E1 } From 8b627030ecd670287143c332481a5712c77984d2 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Fri, 28 Jul 2023 10:41:13 -0400 Subject: [PATCH 8/9] Raname ImperativeDsl to Imperative --- ...tiveDslSpec.scala => ImperativeSpec.scala} | 14 +-- .../{ImperativeDsl.scala => Imperative.scala} | 114 +++++++++--------- 2 files changed, 65 insertions(+), 63 deletions(-) rename core-tests/shared/src/test/scala/zio/prelude/fx/{ImperativeDslSpec.scala => ImperativeSpec.scala} (88%) rename core/shared/src/main/scala/zio/prelude/fx/{ImperativeDsl.scala => Imperative.scala} (56%) diff --git a/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeDslSpec.scala b/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeSpec.scala similarity index 88% rename from core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeDslSpec.scala rename to core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeSpec.scala index 5ba31ac54..860ff1603 100644 --- a/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeDslSpec.scala +++ b/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeSpec.scala @@ -2,11 +2,11 @@ package zio.prelude.fx import zio.prelude._ import zio.test._ -import ImperativeDslSpec.transitSystem.{Dsl, syntax} +import ImperativeSpec.transitSystem.{Dsl, syntax} import Dsl.Card -object ImperativeDslSpec extends ZIOBaseSpec { - def spec: Spec[Environment, Any] = suite("ImperativeDslSpec")( +object ImperativeSpec extends ZIOBaseSpec { + def spec: Spec[Environment, Any] = suite("ImperativeSpec")( suite("unsafeInterpret")( test("Interpreting a getRiderCount after 2 authorized riders") { import syntax._ @@ -54,16 +54,16 @@ object ImperativeDslSpec extends ZIOBaseSpec { object syntax { import Dsl._ - type TSys[+E, +A] = ImperativeDsl[TransitSystemDsl, E, A] + type TSys[+E, +A] = Imperative[TransitSystemDsl, E, A] def authorize(card: Card): TSys[AccessDeniedError, Card] = - ImperativeDsl.eval(Authorize(card)) + Imperative.eval(Authorize(card)) - def getRiderCount: TSys[Nothing, Int] = ImperativeDsl.eval(GetRiderCount) + def getRiderCount: TSys[Nothing, Int] = Imperative.eval(GetRiderCount) } object interpreters { - import ImperativeDsl.Interpreter + import Imperative.Interpreter import Dsl._ type Result[+E, +A] = zio.prelude.fx.ZPure[String, Unit, Unit, Any, E, A] def default(farePriceInCents: Int, initialRiderCount: Int = 0): Interpreter[TransitSystemDsl, Result] = { diff --git a/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala b/core/shared/src/main/scala/zio/prelude/fx/Imperative.scala similarity index 56% rename from core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala rename to core/shared/src/main/scala/zio/prelude/fx/Imperative.scala index c33bd7148..468d22821 100644 --- a/core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala +++ b/core/shared/src/main/scala/zio/prelude/fx/Imperative.scala @@ -16,48 +16,50 @@ package zio.prelude.fx import scala.annotation.tailrec -import ImperativeDsl._ +import Imperative._ /** - * An `ImperativeDsl[Dsl, E, A]` is a data structure that provides the ability to execute a user provided DSL as a sequence of operations. + * An `Imperative[Dsl, E, A]` is a data structure that provides the ability to execute a user provided DSL as a sequence of operations. * From a theoretical standpoint `ImperativeDsl` is an implementation of a Free Monad.`` */ -sealed trait ImperativeDsl[Dsl[+_, +_], +E, +A] { self => +sealed trait Imperative[Dsl[+_, +_], +E, +A] { self => final def catchAll[E2, A1 >: A]( - f: E => ImperativeDsl[Dsl, E2, A1] - ): ImperativeDsl[Dsl, E2, A1] = self match { - case free @ Sequence(fa, onSuccess, onFailure) => + f: E => Imperative[Dsl, E2, A1] + ): Imperative[Dsl, E2, A1] = self match { + case imp @ Sequence(dsl, onSuccess, onFailure) => Sequence( - fa, - (a: free.InSuccess) => onSuccess(a).catchAll(f), - (e: free.InFailure) => onFailure(e).catchAll(f) + dsl, + (a: imp.InSuccess) => onSuccess(a).catchAll(f), + (e: imp.InFailure) => onFailure(e).catchAll(f) ) - case _ => ImperativeDsl.Sequence[Dsl, E, E2, A, A1](self, ImperativeDsl.Succeed(_), f) + case _ => + Sequence[Dsl, E, E2, A, A1](self, Succeed(_), f) } - final def flatMap[E1 >: E, B](f: A => ImperativeDsl[Dsl, E1, B]): ImperativeDsl[Dsl, E1, B] = self match { - case free @ Sequence(fa, onSuccess, onFailure) => + final def flatMap[E1 >: E, B](f: A => Imperative[Dsl, E1, B]): Imperative[Dsl, E1, B] = self match { + case imp @ Sequence(dsl, onSuccess, onFailure) => Sequence( - fa, - (a: free.InSuccess) => + dsl, + (a: imp.InSuccess) => onSuccess(a) .flatMap(f), - (e: free.InFailure) => onFailure(e).flatMap(f) + (e: imp.InFailure) => onFailure(e).flatMap(f) ) - case _ => ImperativeDsl.Sequence[Dsl, E, E1, A, B](self, f, ImperativeDsl.Fail(_)) + case _ => + Sequence[Dsl, E, E1, A, B](self, f, Fail(_)) } - final def flatten[E1 >: E, B](implicit ev: A <:< ImperativeDsl[Dsl, E1, B]): ImperativeDsl[Dsl, E1, B] = + final def flatten[E1 >: E, B](implicit ev: A <:< Imperative[Dsl, E1, B]): Imperative[Dsl, E1, B] = self.flatMap(ev) def interpret[Executable[+_, +_]]( - interpreter: ImperativeDsl.Interpreter[Dsl, Executable] - )(implicit exe: ImperativeDsl.ToExecutable[Executable]): Executable[E, A] = self match { - case ImperativeDsl.Succeed(a) => exe.succeed(a) - case ImperativeDsl.Fail(e) => exe.fail(e) - case ImperativeDsl.Eval(fa) => interpreter.interpret(fa) - case free @ ImperativeDsl.Sequence(fa, onSuccess, onFailure) => + interpreter: Imperative.Interpreter[Dsl, Executable] + )(implicit exe: Imperative.ToExecutable[Executable]): Executable[E, A] = self match { + case Imperative.Succeed(a) => exe.succeed(a) + case Imperative.Fail(e) => exe.fail(e) + case Imperative.Eval(fa) => interpreter.interpret(fa) + case free @ Imperative.Sequence(fa, onSuccess, onFailure) => exe.sequence( fa.interpret(interpreter), (a: free.InSuccess) => onSuccess(a).interpret(interpreter), @@ -65,64 +67,64 @@ sealed trait ImperativeDsl[Dsl[+_, +_], +E, +A] { self => ) } - final def map[B](f: A => B): ImperativeDsl[Dsl, E, B] = - self.flatMap(a => ImperativeDsl.Succeed(f(a))) + final def map[B](f: A => B): Imperative[Dsl, E, B] = + self.flatMap(a => Imperative.Succeed(f(a))) - final def mapError[E2](f: E => E2): ImperativeDsl[Dsl, E2, A] = - self.catchAll(e => ImperativeDsl.Fail(f(e))) + final def mapError[E2](f: E => E2): Imperative[Dsl, E2, A] = + self.catchAll(e => Imperative.Fail(f(e))) def unsafeInterpret( - unsafeInterpreter: ImperativeDsl.UnsafeInterpreter[Dsl] + unsafeInterpreter: Imperative.UnsafeInterpreter[Dsl] ): Either[E, A] = { @tailrec def loop( - free: ImperativeDsl[Dsl, Any, Any], - stack: List[ImperativeDsl.Sequence[Dsl, Any, Any, Any, Any]] + free: Imperative[Dsl, Any, Any], + stack: List[Imperative.Sequence[Dsl, Any, Any, Any, Any]] ): Either[E, A] = free match { - case ImperativeDsl.Succeed(a) => + case Imperative.Succeed(a) => stack match { - case ImperativeDsl.Sequence(_, onSuccess, _) :: stack => loop(onSuccess(a), stack) - case Nil => Right(a.asInstanceOf[A]) + case Imperative.Sequence(_, onSuccess, _) :: stack => loop(onSuccess(a), stack) + case Nil => Right(a.asInstanceOf[A]) } - case ImperativeDsl.Fail(e) => + case Imperative.Fail(e) => stack match { - case ImperativeDsl.Sequence(_, _, onFailure) :: stack => loop(onFailure(e), stack) - case Nil => Left(e.asInstanceOf[E]) + case Imperative.Sequence(_, _, onFailure) :: stack => loop(onFailure(e), stack) + case Nil => Left(e.asInstanceOf[E]) } - case ImperativeDsl.Eval(fa) => + case Imperative.Eval(fa) => unsafeInterpreter.interpret(fa) match { case Left(e) => stack match { - case ImperativeDsl.Sequence(_, _, onFailure) :: stack => loop(onFailure(e), stack) - case Nil => Left(e.asInstanceOf[E]) + case Imperative.Sequence(_, _, onFailure) :: stack => loop(onFailure(e), stack) + case Nil => Left(e.asInstanceOf[E]) } case Right(a) => stack match { - case ImperativeDsl.Sequence(_, onSuccess, _) :: stack => loop(onSuccess(a), stack) - case Nil => Right(a.asInstanceOf[A]) + case Imperative.Sequence(_, onSuccess, _) :: stack => loop(onSuccess(a), stack) + case Nil => Right(a.asInstanceOf[A]) } } - case free @ ImperativeDsl.Sequence(fa, _, _) => - loop(fa, (free :: stack).asInstanceOf[List[ImperativeDsl.Sequence[Dsl, Any, Any, Any, Any]]]) + case free @ Imperative.Sequence(fa, _, _) => + loop(fa, (free :: stack).asInstanceOf[List[Imperative.Sequence[Dsl, Any, Any, Any, Any]]]) } loop(self, Nil) } } -object ImperativeDsl { - def eval[Dsl[+_, +_], E, A](fa: Dsl[E, A]): ImperativeDsl[Dsl, E, A] = Eval(fa) - def fail[Dsl[+_, +_], E](e: E): ImperativeDsl[Dsl, E, Nothing] = Fail(e) - def succeed[Dsl[+_, +_], A](a: A): ImperativeDsl[Dsl, Nothing, A] = Succeed(a) - - final case class Succeed[Dsl[+_, +_], A](a: A) extends ImperativeDsl[Dsl, Nothing, A] - final case class Fail[Dsl[+_, +_], E](a: E) extends ImperativeDsl[Dsl, E, Nothing] - final case class Eval[Dsl[+_, +_], E, A](fa: Dsl[E, A]) extends ImperativeDsl[Dsl, E, A] - final case class Sequence[Dsl[+_, +_], E1, E2, A1, A2] private[ImperativeDsl] ( - dsl: ImperativeDsl[Dsl, E1, A1], - onSuccess: A1 => ImperativeDsl[Dsl, E2, A2], - onFailure: E1 => ImperativeDsl[Dsl, E2, A2] - ) extends ImperativeDsl[Dsl, E2, A2] { +object Imperative { + def eval[Dsl[+_, +_], E, A](fa: Dsl[E, A]): Imperative[Dsl, E, A] = Eval(fa) + def fail[Dsl[+_, +_], E](e: E): Imperative[Dsl, E, Nothing] = Fail(e) + def succeed[Dsl[+_, +_], A](a: A): Imperative[Dsl, Nothing, A] = Succeed(a) + + final case class Succeed[Dsl[+_, +_], A](a: A) extends Imperative[Dsl, Nothing, A] + final case class Fail[Dsl[+_, +_], E](a: E) extends Imperative[Dsl, E, Nothing] + final case class Eval[Dsl[+_, +_], E, A](fa: Dsl[E, A]) extends Imperative[Dsl, E, A] + final case class Sequence[Dsl[+_, +_], E1, E2, A1, A2] private[Imperative] ( + dsl: Imperative[Dsl, E1, A1], + onSuccess: A1 => Imperative[Dsl, E2, A2], + onFailure: E1 => Imperative[Dsl, E2, A2] + ) extends Imperative[Dsl, E2, A2] { type InSuccess = A1 type InFailure = E1 } From cea5b616e29b34f00254bb97c600bbca922ef558 Mon Sep 17 00:00:00 2001 From: Damian Reeves <957246+DamianReeves@users.noreply.github.com> Date: Fri, 28 Jul 2023 17:49:10 -0400 Subject: [PATCH 9/9] Fix tests to use new imperative syntax --- .../scala/zio/prelude/fx/ImperativeSpec.scala | 26 ++-- .../scala/zio/prelude/fx/Imperative.scala | 141 +++++++++--------- 2 files changed, 81 insertions(+), 86 deletions(-) diff --git a/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeSpec.scala b/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeSpec.scala index 860ff1603..6add89bb6 100644 --- a/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeSpec.scala +++ b/core-tests/shared/src/test/scala/zio/prelude/fx/ImperativeSpec.scala @@ -4,13 +4,14 @@ import zio.prelude._ import zio.test._ import ImperativeSpec.transitSystem.{Dsl, syntax} import Dsl.Card +import zio.prelude.EReader object ImperativeSpec extends ZIOBaseSpec { def spec: Spec[Environment, Any] = suite("ImperativeSpec")( suite("unsafeInterpret")( test("Interpreting a getRiderCount after 2 authorized riders") { import syntax._ - case class Customer(name:String, card:Card) + case class Customer(name: String, card: Card) val john = Customer("John", Card.TransitRideCard(2)) val jane = Customer("Jane", Card.DebitCard(1000)) @@ -18,8 +19,8 @@ object ImperativeSpec extends ZIOBaseSpec { val interpreter = transitSystem.interpreters.default(farePriceInCents = 250) val program = for { - _ <- authorize(john.card) - _ <- authorize(jane.card) + _ <- authorize(john.card) + _ <- authorize(jane.card) cnt <- getRiderCount } yield cnt @@ -33,9 +34,9 @@ object ImperativeSpec extends ZIOBaseSpec { object transitSystem { object Dsl { - sealed trait TransitSystemDsl[+E, +A] extends Product with Serializable - final case class Authorize(card: Card) extends TransitSystemDsl[AccessDeniedError, Card] - case object GetRiderCount extends TransitSystemDsl[Nothing, Int] + sealed trait TransitSystemDsl[-R, +E, +A] extends Product with Serializable + final case class Authorize(card: Card) extends TransitSystemDsl[Any, AccessDeniedError, Card] + case object GetRiderCount extends TransitSystemDsl[Any, Nothing, Int] sealed trait Card object Card { @@ -54,22 +55,21 @@ object ImperativeSpec extends ZIOBaseSpec { object syntax { import Dsl._ - type TSys[+E, +A] = Imperative[TransitSystemDsl, E, A] + type TSys[-R, +E, +A] = Imperative[TransitSystemDsl, R, E, A] - def authorize(card: Card): TSys[AccessDeniedError, Card] = + def authorize(card: Card): TSys[Any, AccessDeniedError, Card] = Imperative.eval(Authorize(card)) - def getRiderCount: TSys[Nothing, Int] = Imperative.eval(GetRiderCount) + def getRiderCount: TSys[Any, Nothing, Int] = Imperative.eval(GetRiderCount) } object interpreters { import Imperative.Interpreter import Dsl._ - type Result[+E, +A] = zio.prelude.fx.ZPure[String, Unit, Unit, Any, E, A] - def default(farePriceInCents: Int, initialRiderCount: Int = 0): Interpreter[TransitSystemDsl, Result] = { + def default(farePriceInCents: Int, initialRiderCount: Int = 0): Interpreter[TransitSystemDsl, EReader] = { var riderCount = initialRiderCount - new Interpreter[TransitSystemDsl, Result] { - override def interpret[E, A](dsl: TransitSystemDsl[E, A]): Result[E, A] = + new Interpreter[TransitSystemDsl, EReader] { + override def interpret[R, E, A](dsl: TransitSystemDsl[R, E, A]): EReader[R, E, A] = dsl match { case Authorize(card) => card match { diff --git a/core/shared/src/main/scala/zio/prelude/fx/Imperative.scala b/core/shared/src/main/scala/zio/prelude/fx/Imperative.scala index 468d22821..f3843aec1 100644 --- a/core/shared/src/main/scala/zio/prelude/fx/Imperative.scala +++ b/core/shared/src/main/scala/zio/prelude/fx/Imperative.scala @@ -14,7 +14,7 @@ * limitations under the License. */ package zio.prelude.fx - +import zio.prelude.EReader import scala.annotation.tailrec import Imperative._ @@ -22,11 +22,11 @@ import Imperative._ * An `Imperative[Dsl, E, A]` is a data structure that provides the ability to execute a user provided DSL as a sequence of operations. * From a theoretical standpoint `ImperativeDsl` is an implementation of a Free Monad.`` */ -sealed trait Imperative[Dsl[+_, +_], +E, +A] { self => +sealed trait Imperative[Dsl[-_, +_, +_], -R, +E, +A] { self => - final def catchAll[E2, A1 >: A]( - f: E => Imperative[Dsl, E2, A1] - ): Imperative[Dsl, E2, A1] = self match { + final def catchAll[R1 <: R, E2, A1 >: A]( + f: E => Imperative[Dsl, R1, E2, A1] + ): Imperative[Dsl, R1, E2, A1] = self match { case imp @ Sequence(dsl, onSuccess, onFailure) => Sequence( dsl, @@ -34,10 +34,10 @@ sealed trait Imperative[Dsl[+_, +_], +E, +A] { self => (e: imp.InFailure) => onFailure(e).catchAll(f) ) case _ => - Sequence[Dsl, E, E2, A, A1](self, Succeed(_), f) + Sequence[Dsl, R1, E, E2, A, A1](self, Succeed(_), f) } - final def flatMap[E1 >: E, B](f: A => Imperative[Dsl, E1, B]): Imperative[Dsl, E1, B] = self match { + final def flatMap[R1 <: R, E1 >: E, B](f: A => Imperative[Dsl, R1, E1, B]): Imperative[Dsl, R1, E1, B] = self match { case imp @ Sequence(dsl, onSuccess, onFailure) => Sequence( dsl, @@ -47,15 +47,15 @@ sealed trait Imperative[Dsl[+_, +_], +E, +A] { self => (e: imp.InFailure) => onFailure(e).flatMap(f) ) case _ => - Sequence[Dsl, E, E1, A, B](self, f, Fail(_)) + Sequence[Dsl, R1, E, E1, A, B](self, f, Fail(_)) } - final def flatten[E1 >: E, B](implicit ev: A <:< Imperative[Dsl, E1, B]): Imperative[Dsl, E1, B] = + final def flatten[R1 <: R, E1 >: E, B](implicit ev: A <:< Imperative[Dsl, R1, E1, B]): Imperative[Dsl, R1, E1, B] = self.flatMap(ev) - def interpret[Executable[+_, +_]]( + def interpret[Executable[-_, +_, +_]]( interpreter: Imperative.Interpreter[Dsl, Executable] - )(implicit exe: Imperative.ToExecutable[Executable]): Executable[E, A] = self match { + )(implicit exe: Imperative.ToExecutable[Executable]): Executable[R, E, A] = self match { case Imperative.Succeed(a) => exe.succeed(a) case Imperative.Fail(e) => exe.fail(e) case Imperative.Eval(fa) => interpreter.interpret(fa) @@ -67,10 +67,10 @@ sealed trait Imperative[Dsl[+_, +_], +E, +A] { self => ) } - final def map[B](f: A => B): Imperative[Dsl, E, B] = + final def map[B](f: A => B): Imperative[Dsl, R, E, B] = self.flatMap(a => Imperative.Succeed(f(a))) - final def mapError[E2](f: E => E2): Imperative[Dsl, E2, A] = + final def mapError[E2](f: E => E2): Imperative[Dsl, R, E2, A] = self.catchAll(e => Imperative.Fail(f(e))) def unsafeInterpret( @@ -78,8 +78,8 @@ sealed trait Imperative[Dsl[+_, +_], +E, +A] { self => ): Either[E, A] = { @tailrec def loop( - free: Imperative[Dsl, Any, Any], - stack: List[Imperative.Sequence[Dsl, Any, Any, Any, Any]] + free: Imperative[Dsl, R, Any, Any], + stack: List[Imperative.Sequence[Dsl, R, Any, Any, Any, Any]] ): Either[E, A] = free match { case Imperative.Succeed(a) => @@ -106,90 +106,85 @@ sealed trait Imperative[Dsl[+_, +_], +E, +A] { self => } } case free @ Imperative.Sequence(fa, _, _) => - loop(fa, (free :: stack).asInstanceOf[List[Imperative.Sequence[Dsl, Any, Any, Any, Any]]]) + loop(fa, (free :: stack).asInstanceOf[List[Imperative.Sequence[Dsl, R, Any, Any, Any, Any]]]) } loop(self, Nil) } } object Imperative { - def eval[Dsl[+_, +_], E, A](fa: Dsl[E, A]): Imperative[Dsl, E, A] = Eval(fa) - def fail[Dsl[+_, +_], E](e: E): Imperative[Dsl, E, Nothing] = Fail(e) - def succeed[Dsl[+_, +_], A](a: A): Imperative[Dsl, Nothing, A] = Succeed(a) - - final case class Succeed[Dsl[+_, +_], A](a: A) extends Imperative[Dsl, Nothing, A] - final case class Fail[Dsl[+_, +_], E](a: E) extends Imperative[Dsl, E, Nothing] - final case class Eval[Dsl[+_, +_], E, A](fa: Dsl[E, A]) extends Imperative[Dsl, E, A] - final case class Sequence[Dsl[+_, +_], E1, E2, A1, A2] private[Imperative] ( - dsl: Imperative[Dsl, E1, A1], - onSuccess: A1 => Imperative[Dsl, E2, A2], - onFailure: E1 => Imperative[Dsl, E2, A2] - ) extends Imperative[Dsl, E2, A2] { + def eval[Dsl[-_, +_, +_], R, E, A](dsl: Dsl[R, E, A]): Imperative[Dsl, R, E, A] = Eval(dsl) + def fail[Dsl[-_, +_, +_], E](error: E): Imperative[Dsl, Any, E, Nothing] = Fail(error) + def succeed[Dsl[-_, +_, +_], A](value: A): Imperative[Dsl, Any, Nothing, A] = Succeed(value) + + final case class Succeed[Dsl[-_, +_, +_], A](a: A) extends Imperative[Dsl, Any, Nothing, A] + final case class Fail[Dsl[-_, +_, +_], E](error: E) extends Imperative[Dsl, Any, E, Nothing] + final case class Eval[Dsl[-_, +_, +_], R, E, A](dsl: Dsl[R, E, A]) extends Imperative[Dsl, R, E, A] + final case class Sequence[Dsl[-_, +_, +_], R, E1, E2, A1, A2] private[Imperative] ( + dsl: Imperative[Dsl, R, E1, A1], + onSuccess: A1 => Imperative[Dsl, R, E2, A2], + onFailure: E1 => Imperative[Dsl, R, E2, A2] + ) extends Imperative[Dsl, R, E2, A2] { type InSuccess = A1 type InFailure = E1 } /// Interpreter provides the ability to interpret a DSL into an executable program - trait Interpreter[Dsl[+_, +_], Executable[+_, +_]] { self => - def interpret[E, A](dsl: Dsl[E, A]): Executable[E, A] - - def combine[Dsl2[+_, +_]]( - that: Interpreter[Dsl2, Executable] - ): Interpreter[({ type lambda[+E, +A] = CompositeDsl[Dsl, Dsl2, E, A] })#lambda, Executable] = - new Interpreter[({ type lambda[+E, +A] = CompositeDsl[Dsl, Dsl2, E, A] })#lambda, Executable] { - override def interpret[E, A](dsl: CompositeDsl[Dsl, Dsl2, E, A]): Executable[E, A] = dsl.eitherDsl match { - case Left(dsl) => self.interpret(dsl) - case Right(dsl) => that.interpret(dsl) - } - } + trait Interpreter[Dsl[-_, +_, +_], Executable[-_, +_, +_]] { self => + def interpret[R, E, A](dsl: Dsl[R, E, A]): Executable[R, E, A] + +// def combine[R2, Dsl2[-_, +_, +_]]( +// that: Interpreter[Dsl2, Executable] +// ): Interpreter[({ type lambda[-R, +E, +A] = CompositeDsl[Dsl, Dsl2, R, R2, E, A] })#lambda, Executable] = +// new Interpreter[({ type lambda[-R, +E, +A] = CompositeDsl[Dsl, Dsl2, R, R2, E, A] })#lambda, Executable] { +// override def interpret[R, E, A](dsl: CompositeDsl[Dsl, Dsl2, R, R2, E, A]): Executable[R with R2, E, A] = +// dsl.eitherDsl match { +// case Left(dsl) => self.interpret(dsl) +// case Right(dsl) => that.interpret(dsl) +// } +// } } - trait UnsafeInterpreter[Dsl[+_, +_]] { - def interpret[E, A](fa: Dsl[E, A]): Either[E, A] + trait UnsafeInterpreter[Dsl[-_, +_, +_]] { + def interpret[R, E, A](dsl: Dsl[R, E, A]): Either[E, A] } - trait ToExecutable[Executable[+_, +_]] { - def succeed[A](a: A): Executable[Nothing, A] - def fail[E](e: E): Executable[E, Nothing] - def eval[E, A](fa: Executable[E, A]): Executable[E, A] - def sequence[E1, E2, A1, A2]( - fa: Executable[E1, A1], - onSuccess: A1 => Executable[E2, A2], - onFailure: E1 => Executable[E2, A2] - ): Executable[E2, A2] + trait ToExecutable[Executable[-_, +_, +_]] { + def succeed[A](a: A): Executable[Any, Nothing, A] + def fail[E](e: E): Executable[Any, E, Nothing] + def eval[R, E, A](fa: Executable[R, E, A]): Executable[R, E, A] + def sequence[R, E1, E2, A1, A2]( + fa: Executable[R, E1, A1], + onSuccess: A1 => Executable[R, E2, A2], + onFailure: E1 => Executable[R, E2, A2] + ): Executable[R, E2, A2] } - final case class CompositeDsl[+Dsl1[+_, +_], +Dsl2[+_, +_], +E, +A](eitherDsl: Either[Dsl1[E, A], Dsl2[E, A]]) - extends AnyVal { self => - type InSuccess <: A - type InFailure <: E - } +// final case class CompositeDsl[+Dsl1[-_, +_, +_], +Dsl2[-_, +_, +_], -R1, -R2, +E, +A]( +// eitherDsl: Either[Dsl1[R1, E, A], Dsl2[R2, E, A]] +// ) extends AnyVal { self => +// type InSuccess <: A +// type InFailure <: E +// } - // TODO: Consider what can be done to make the type lambda here simpler - implicit def ZPureToExecutable[W] - : ToExecutable[({ type lambda[+E, +A] = ZPure[W, Unit, Unit, Any, E, A] })#lambda] = { - // ({ type lambda[+E, +A] = ZPure[W, Unit, Unit, Any, E, A] })#lambda - type Result[+E, +A] = ZPure[W, Unit, Unit, Any, E, A] - val Result: zio.prelude.fx.ZPure.type = zio.prelude.fx.ZPure - new ToExecutable[Result] { + implicit def ZPureToExecutable: ToExecutable[EReader] = + new ToExecutable[EReader] { - override def succeed[A](a: A): Result[Nothing, A] = Result.succeed(a) + override def succeed[A](a: A): EReader[Any, Nothing, A] = EReader.succeed(a) - override def fail[E](e: E): Result[E, Nothing] = Result.fail(e) + override def fail[E](e: E): EReader[Any, E, Nothing] = EReader.fail(e) - override def eval[E, A](fa: Result[E, A]): Result[E, A] = Result.suspend(fa) + override def eval[R, E, A](fa: EReader[R, E, A]): EReader[R, E, A] = EReader.suspend(fa) - override def sequence[E1, E2, A1, A2]( - fa: Result[E1, A1], - onSuccess: A1 => Result[E2, A2], - onFailure: E1 => Result[E2, A2] - ): Result[E2, A2] = Result.suspend { - // TODO: Consider if this can be done with foldM since its pissible E1 or E2 is Nothing + override def sequence[R, E1, E2, A1, A2]( + fa: EReader[R, E1, A1], + onSuccess: A1 => EReader[R, E2, A2], + onFailure: E1 => EReader[R, E2, A2] + ): EReader[R, E2, A2] = EReader.suspend { fa.foldM( onFailure, onSuccess ) } } - } }