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

Eval refactor continued #218

Merged
merged 5 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
scala: [2.12.17, 2.13.10, 3.2.2]
scala: [2.12.18, 2.13.12, 3.3.1]
java: [temurin@8, temurin@17]
runs-on: ${{ matrix.os }}
steps:
Expand Down Expand Up @@ -61,4 +61,4 @@ jobs:
with:
fetch-depth: 0
- uses: olafurpg/setup-scala@v13
- run: sbt '++2.12.17 docs/mdoc'
- run: sbt '++2.12.18 docs/mdoc'
8 changes: 4 additions & 4 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ val isDotty = Def.setting(CrossVersion.partialVersion(scalaVersion.value).exists

// Dependencies

val catsVersion = "2.9.0"
val catsVersion = "2.10.0"
val castsTestkitScalatestVersion = "2.1.5"

libraryDependencies ++= Seq(
Expand All @@ -19,9 +19,9 @@ libraryDependencies ++= (if (isDotty.value) Nil

// Multiple Scala versions support

val scala_2_12 = "2.12.17"
val scala_2_13 = "2.13.10"
val dotty = "3.2.2"
val scala_2_12 = "2.12.18"
val scala_2_13 = "2.13.12"
val dotty = "3.3.1"
val mainScalaVersion = scala_2_13
val supportedScalaVersions = Seq(scala_2_12, scala_2_13, dotty)

Expand Down
177 changes: 103 additions & 74 deletions src/main/scala/pl/iterators/sealedmonad/Sealed.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ import cats._
import cats.syntax.all._

import scala.Function.const
import scala.language.higherKinds

sealed trait Sealed[F[_], +A, +ADT] {
import Sealed._
def map[B](f: A => B): Sealed[F, B, ADT] = FlatMap(this, (a: A) => Intermediate(f(a)))
def flatMap[B, ADT1 >: ADT](f: A => Sealed[F, B, ADT1]): Sealed[F, B, ADT1] = FlatMap(this, f)
def map[B](f: A => B): Sealed[F, B, ADT] = Transform(this, f.andThen(left[F, B, ADT]), right[F, B, ADT])
def flatMap[B, ADT1 >: ADT](f: A => Sealed[F, B, ADT1]): Sealed[F, B, ADT1] = Transform(this, f, right[F, B, ADT1])

/** Transforms `A` to `B` using an effectful function.
*
Expand All @@ -26,10 +25,10 @@ sealed trait Sealed[F[_], +A, +ADT] {
* res0: cats.Id[Response] = Value(42)
* }}}
*/
final def semiflatMap[B](f: A => F[B]): Sealed[F, B, ADT] = flatMap(a => Sealed.IntermediateF(Eval.later(f(a))))
final def semiflatMap[B](f: A => F[B]): Sealed[F, B, ADT] = Transform(this, f.andThen(leftF), right[F, B, ADT])

final def leftSemiflatMap[ADT1 >: ADT](f: ADT => F[ADT1]): Sealed[F, A, ADT1] =
foldM[A, ADT]((adt: ADT) => ResultF(Eval.later(f(adt))).asInstanceOf[Sealed[F, A, ADT]], a => Intermediate(a))
Transform(this, left[F, A, ADT1], f.andThen(rightF))

/** Executes a side effect if ADT has been reached, and returns unchanged `Sealed[F, A, ADT]`.
*
Expand All @@ -51,18 +50,16 @@ sealed trait Sealed[F[_], +A, +ADT] {
* }}}
*/
final def leftSemiflatTap[C](f: ADT => F[C]): Sealed[F, A, ADT] =
foldM[A, ADT](
(adt: ADT) => IntermediateF(Eval.later(f(adt))).flatMap(_ => Result(adt)),
a => Intermediate(a)
Transform(
this,
left[F, A, ADT],
(adt: ADT) => Transform(leftF(f(adt)), (_: C) => right[F, A, ADT](adt), (_: Any) => right(adt))
)

/** Combine leftSemiflatMap and semiflatMap together.
*/
final def biSemiflatMap[B, ADT1 >: ADT](fa: ADT => F[ADT1], fb: A => F[B]): Sealed[F, B, ADT1] =
foldM[B, ADT](
(adt: ADT) => ResultF(Eval.later(fa(adt))).asInstanceOf[Sealed[F, B, ADT]],
a => IntermediateF(Eval.later(fb(a))).asInstanceOf[Sealed[F, B, ADT]]
)
Transform(this, (a: A) => leftF[F, B, ADT1](fb(a)), (adt: ADT) => rightF[F, B, ADT1](fa(adt)))

/** Executes appropriate side effect depending on whether `A` or `ADT` has been reached, and returns unchanged `Sealed[F, A, ADT]`.
*
Expand All @@ -85,9 +82,10 @@ sealed trait Sealed[F[_], +A, +ADT] {
* }}}
*/
final def biSemiflatTap[B, C](fa: ADT => F[C], fb: A => F[B]): Sealed[F, A, ADT] =
foldM[A, ADT](
(adt: ADT) => IntermediateF(Eval.later(fa(adt))).flatMap(_ => Result(adt)),
a => IntermediateF(Eval.later(fb(a))).flatMap(_ => Intermediate(a))
Transform(
this,
(a: A) => Transform(leftF[F, B, ADT](fb(a)), (_: B) => left[F, A, ADT](a), (adt: ADT) => right[F, A, ADT](adt)),
(adt: ADT) => Transform(leftF[F, C, ADT](fa(adt)), (_: C) => right[F, A, ADT](adt), (adt: ADT) => right[F, A, ADT](adt))
)

/** Finishes the computation by returning Sealed with given ADT.
Expand All @@ -106,7 +104,7 @@ sealed trait Sealed[F[_], +A, +ADT] {
* res0: cats.Id[Response] = Transformed(2)
* }}}
*/
final def complete[ADT1 >: ADT](f: A => ADT1): Sealed[F, Nothing, ADT1] = flatMap(a => Result(f(a)))
final def complete[ADT1 >: ADT](f: A => ADT1): Sealed[F, Nothing, ADT1] = flatMap(a => right(f(a)))

/** Effectful version of `complete`.
*
Expand All @@ -124,13 +122,13 @@ sealed trait Sealed[F[_], +A, +ADT] {
* res0: cats.Id[Response] = Transformed(2)
* }}}
*/
final def completeWith[ADT1 >: ADT](f: A => F[ADT1]): Sealed[F, Nothing, ADT1] = flatMap(a => Sealed.ResultF(Eval.later(f(a))))
final def completeWith[ADT1 >: ADT](f: A => F[ADT1]): Sealed[F, Nothing, ADT1] = flatMap(f.andThen(rightF))

/** Converts `Sealed[F, Either[ADT1, B], ADT]` into `Sealed[F, B, ADT1]`. Usually paired with `either`. See `Sealed#either` for example
* usage.
*/
final def rethrow[B, ADT1 >: ADT](implicit ev: A <:< Either[ADT1, B]): Sealed[F, B, ADT1] =
flatMap(a => ev(a).fold(Result(_), Intermediate(_)))
flatMap(a => ev(a).fold(right, left))

/** Converts `A` into `Either[ADT1, B]` and creates a Sealed instance from the result.
*
Expand Down Expand Up @@ -198,7 +196,7 @@ sealed trait Sealed[F[_], +A, +ADT] {
* }}}
*/
final def foldM[B, ADT1 >: ADT](left: ADT => Sealed[F, B, ADT1], right: A => Sealed[F, B, ADT1]): Sealed[F, B, ADT1] =
Fold(this, right, left.asInstanceOf[ADT1 => Sealed[F, B, ADT1]])
Transform(this, right, left)

/** Converts `A` into `Either[ADT, A]`. Usually paired with `rethrow`.
*
Expand All @@ -220,7 +218,7 @@ sealed trait Sealed[F[_], +A, +ADT] {
* }}}
*/
final def either: Sealed[F, Either[ADT, A], ADT] =
foldM((adt: ADT) => Intermediate(Either.left(adt)), a => Intermediate(Either.right(a)))
foldM(adt => left(Either.left(adt)), a => left(Either.right(a)))

/** Executes a fire-and-forget side effect and returns unchanged `Sealed[F, A, ADT]`. Works irrespectively of Sealed's current state, in
* contrary to `tap`. Useful for logging purposes.
Expand Down Expand Up @@ -325,7 +323,7 @@ sealed trait Sealed[F[_], +A, +ADT] {
*/

final def ensureOrF[ADT1 >: ADT](pred: A => Boolean, orElse: A => F[ADT1]): Sealed[F, A, ADT1] =
flatMap(a => if (pred(a)) Sealed.Intermediate(a) else completeWith(orElse))
flatMap(a => if (pred(a)) left(a) else completeWith(orElse))

/** Effectful version of `ensure`.
*
Expand Down Expand Up @@ -420,87 +418,118 @@ sealed trait Sealed[F[_], +A, +ADT] {
* }}}
*/
final def flatTapWhen[B](cond: A => Boolean, f: A => F[B]): Sealed[F, A, ADT] =
flatMap(a => if (cond(a)) flatTap(f) else Sealed.Intermediate(a))

private def feval[A1 >: A, ADT1 >: ADT](implicit
F: Monad[F]
): Eval[F[Either[A1, ADT1]]] = this match {
case Intermediate(value) => Eval.later(value.asLeft[ADT1].pure[F]).asInstanceOf[Eval[F[Either[A1, ADT1]]]]
case IntermediateF(value) => value.map(_.map(_.asLeft[ADT1]))
case Result(value) => Eval.later(value.asRight[A1].pure[F]).asInstanceOf[Eval[F[Either[A1, ADT1]]]]
case ResultF(value) => value.map(_.map(_.asRight[A1]))
case FlatMap(current, next) =>
current.feval
.map { feither =>
feither.flatMap {
case scala.Left(value) =>
next(value).feval[A1, ADT1].value
case either =>
either.pure[F].asInstanceOf[F[Either[A1, ADT1]]]
}
}
.asInstanceOf[Eval[F[Either[A1, ADT1]]]]
case Fold(current, left, right) =>
current.feval
.map { feither =>
feither.flatMap {
case scala.Left(value) =>
left(value).feval[A1, ADT1].value
case scala.Right(value) =>
right(value).feval[A1, ADT1].value
}
}
.asInstanceOf[Eval[F[Either[A1, ADT1]]]]
}
flatMap(a => if (cond(a)) flatTap(f) else left(a))

final def run[ADT1 >: ADT](implicit ev: A <:< ADT1, F: Monad[F]): F[ADT1] = feval[A, ADT].value.map(_.fold(ev, identity))
final def run[ADT1 >: ADT](implicit ev: A <:< ADT1, F: Monad[F]): F[ADT1] = eval(this).map(_.fold(ev, identity))
}

object Sealed extends SealedInstances {

import cats.syntax.either._

def apply[F[_], A](value: => F[A]): Sealed[F, A, Nothing] = IntermediateF(Eval.later(value))
def liftF[F[_], A](value: A): Sealed[F, A, Nothing] = Intermediate(value)
def apply[F[_], A](value: => F[A]): Sealed[F, A, Nothing] = defer(leftF(value))
def liftF[F[_], A](value: A): Sealed[F, A, Nothing] = defer(left(value))

def seal[F[_], A](value: A): Sealed[F, Nothing, A] = Result(value)
def seal[F[_], A](value: A): Sealed[F, Nothing, A] = defer(right(value))

def result[F[_], ADT](value: => F[ADT]): Sealed[F, Nothing, ADT] = ResultF(Eval.later(value))
def result[F[_], ADT](value: => F[ADT]): Sealed[F, Nothing, ADT] = defer(rightF(value))

def valueOr[F[_], A, ADT](fa: => F[Option[A]], orElse: => ADT): Sealed[F, A, ADT] = apply(fa).flatMap {
case Some(a) => Intermediate(a)
case None => Result(orElse)
case Some(a) => left(a)
case None => right(orElse)
}

def valueOrF[F[_], A, ADT](fa: => F[Option[A]], orElse: => F[ADT]): Sealed[F, A, ADT] =
apply(fa).flatMap {
case Some(a) => liftF(a)
case None => result(orElse)
case Some(a) => left(a)
case None => rightF(orElse)
}

def handleError[F[_], A, B, ADT](fa: F[Either[A, B]])(f: A => ADT): Sealed[F, B, ADT] = apply(fa).attempt(_.leftMap(f))

def bimap[F[_], A, B, C, ADT](fa: F[Either[A, B]])(f: A => ADT)(fb: B => C): Sealed[F, C, ADT] =
apply(fa).attempt(_.leftMap(f).map(fb))

private final case class Intermediate[F[_], A](value: A) extends Sealed[F, A, Nothing]

private final case class IntermediateF[F[_], A](value: Eval[F[A]]) extends Sealed[F, A, Nothing]

private final case class Result[F[_], ADT](value: ADT) extends Sealed[F, Nothing, ADT]
/** Represents either an intermediate A or a final ADT.
*/
private final case class Pure[F[_], A, ADT](
value: Either[A, ADT]
) extends Sealed[F, A, ADT]

private final case class ResultF[F[_], ADT](value: Eval[F[ADT]]) extends Sealed[F, Nothing, ADT]
/** Represents an intermediate F[A] or a final F[ADT].
*/
private final case class Suspend[F[_], A, ADT](
fa: Either[F[A], F[ADT]]
) extends Sealed[F, A, ADT]

private final case class FlatMap[F[_], A0, A, ADT](
current: Sealed[F, A0, ADT],
next: A0 => Sealed[F, A, ADT]
/** Represents a deferred computation.
*/
private final case class Defer[F[_], A, ADT](
value: () => Sealed[F, A, ADT]
) extends Sealed[F, A, ADT]

private final case class Fold[F[_], A0, A, ADT](
current: Sealed[F, A0, ADT],
/** Represents a transformation on either intermediate A0 or final ADT0 value.
*
* Mind that the naming here might be a bit confusing because `left` is a transformation that is applied when we haven't reached the
* final ADT yet, and `right` is a transformation that is applied when we have reached the final ADT.
*
* On the user side Sealed behaves similar to EitherT, so `left` applies to final ADT and right applies to intermediate A. See `foldM`
* for an example.
*/
private final case class Transform[F[_], A0, A, ADT0, ADT](
current: Sealed[F, A0, ADT0],
left: A0 => Sealed[F, A, ADT],
right: ADT => Sealed[F, A, ADT]
right: ADT0 => Sealed[F, A, ADT]
) extends Sealed[F, A, ADT]

private def left[F[_], A, ADT](value: A): Sealed[F, A, ADT] = Pure(Left(value))
private def leftF[F[_], A, ADT](value: F[A]): Sealed[F, A, ADT] = Suspend(Left(value))
private def right[F[_], A, ADT](value: ADT): Sealed[F, A, ADT] = Pure(Right(value))
private def rightF[F[_], A, ADT](value: F[ADT]): Sealed[F, A, ADT] = Suspend(Right(value))
private def defer[F[_], A, ADT](thunk: => Sealed[F, A, ADT]) = Defer(() => thunk)

/** Does the heavy lifting. There's a trampoline to advance only one step forward. Transform is unrolled and rewritten to avoid nested
* functions and offer stack safety.
*/
private def eval[F[_], A, ADT](value: Sealed[F, A, ADT])(implicit F: Monad[F]): F[Either[A, ADT]] = {
type Intermediate = Sealed[F, A, ADT]
type Final = Either[A, ADT]
def recur(value: Intermediate): F[Either[Intermediate, Final]] = value.asLeft[Final].pure[F]
def returns(value: Final): F[Either[Intermediate, Final]] = value.asRight[Intermediate].pure[F]
value.tailRecM {
case Pure(either) => returns(either)
case Suspend(Left(fa)) => fa.flatMap(a => returns(a.asLeft[ADT]))
case Suspend(Right(fadt)) => fadt.flatMap(adt => returns(adt.asRight[A]))
case Defer(value) => recur(value())
case Transform(current, onA, onADT) =>
current match {
case Pure(Left(a)) => recur(onA(a))
case Pure(Right(adt)) => recur(onADT(adt))
case Suspend(Left(fa)) => fa.flatMap(a => recur(Transform(Pure(Left(a)), onA, onADT)))
case Suspend(Right(fadt)) => fadt.flatMap(adt => recur(Transform(Pure(Right(adt)), onA, onADT)))
case Defer(value) => recur(Transform(value(), onA, onADT))
case Transform(next, onA0, onADT0) =>
// the asInstanceOf below are for cross Scala 2/3 compatibility and can be avoided when src code would be split
recur(
Transform[F, Any, A, Any, ADT](
next,
(a0: Any) =>
Transform[F, Any, A, Any, ADT](
defer(onA0.asInstanceOf[Any => Sealed[F, A, ADT]](a0)),
onA.asInstanceOf[Any => Sealed[F, A, ADT]],
onADT.asInstanceOf[Any => Sealed[F, A, ADT]]
),
(adt0: Any) =>
Transform[F, Any, A, Any, ADT](
defer(onADT0.asInstanceOf[Any => Sealed[F, Any, Any]](adt0)),
onA.asInstanceOf[Any => Sealed[F, A, ADT]],
onADT.asInstanceOf[Any => Sealed[F, A, ADT]]
)
)
)
}
}
}
}

private final class SealedMonad[F[_], ADT] extends StackSafeMonad[Sealed[F, *, ADT]] {
Expand Down
Loading