Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding a Free Monad implementation but with a zio.prelude flare #1171

Draft
wants to merge 9 commits into
base: series/2.x
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package zio.prelude.fx

import zio.prelude._
import zio.test._
import ImperativeDslSpec.transitSystem.{Dsl, syntax}
import Dsl.Card

object ImperativeDslSpec extends ZIOBaseSpec {
def spec: Spec[Environment, Any] = suite("ImperativeDslSpec")(
suite("unsafeInterpret")(
test("Interpreting a getRiderCount after 2 authorized riders") {
import syntax._
case class Customer(name:String, card:Card)

val john = Customer("John", Card.TransitRideCard(2))
val jane = Customer("Jane", Card.DebitCard(1000))

val interpreter = transitSystem.interpreters.default(farePriceInCents = 250)

val program = for {
_ <- authorize(john.card)
_ <- authorize(jane.card)
cnt <- getRiderCount
} yield cnt

val result = program.interpret(interpreter)
val actual = result.runEither

assertTrue(actual == Right(2))
}
)
)

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 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 syntax {
import Dsl._

type TSys[+E, +A] = ImperativeDsl[TransitSystemDsl, 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 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] = {
var riderCount = initialRiderCount
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) =>
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)
}
}
}
}
}
}
181 changes: 181 additions & 0 deletions core/shared/src/main/scala/zio/prelude/fx/ImperativeDsl.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package zio.prelude.fx

import scala.annotation.tailrec
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 =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might call this Imperative just to make it a little shorter.


final def catchAll[E2, A1 >: A](
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.InFailure) => onFailure(e).catchAll(f)
)
case _ => ImperativeDsl.Sequence[Dsl, E, E2, A, A1](self, ImperativeDsl.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) =>
Sequence(
fa,
(a: free.InSuccess) =>
onSuccess(a)
.flatMap(f),
(e: free.InFailure) => onFailure(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[Dsl, E1, B]): ImperativeDsl[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.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.InFailure) => onFailure(e).interpret(interpreter)
)
}

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[Dsl, E2, A] =
self.catchAll(e => ImperativeDsl.Opail(f(e)))

def unsafeInterpret(
unsafeInterpreter: ImperativeDsl.UnsafeInterpreter[Dsl]
): Either[E, A] = {
@tailrec
def loop(
free: ImperativeDsl[Dsl, Any, Any],
stack: List[ImperativeDsl.Sequence[Dsl, 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(_, _, 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(_, _, 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 free @ ImperativeDsl.Sequence(fa, _, _) =>
loop(fa, (free :: stack).asInstanceOf[List[ImperativeDsl.Sequence[Dsl, 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]
DamianReeves marked this conversation as resolved.
Show resolved Hide resolved
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] {
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 UnsafeInterpreter[Dsl[+_, +_]] {
def interpret[E, A](fa: Dsl[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]
}

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use EWriter or another appropriate ZPure type alias.

implicit def ZPureToExecutable[W]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No point to the W parameter here unless you want to add other cases to Imperative. There is no way to interact with it.

: 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete.

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] {

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

???

fa.foldM(
onFailure,
onSuccess
)
}
}
}
}