Skip to content

Commit

Permalink
Eval refactor continued (#218)
Browse files Browse the repository at this point in the history
* WIP with tailRecM

* Cleanup

* Working version. Some documentation. Dependency updates.

* Missing build changes.

* Website 2.12 update.
luksow authored Jan 3, 2024
1 parent f67c5c5 commit aef0473
Showing 3 changed files with 109 additions and 80 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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:
@@ -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
@@ -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(
@@ -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)

177 changes: 103 additions & 74 deletions src/main/scala/pl/iterators/sealedmonad/Sealed.scala
Original file line number Diff line number Diff line change
@@ -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.
*
@@ -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]`.
*
@@ -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]`.
*
@@ -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.
@@ -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`.
*
@@ -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.
*
@@ -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`.
*
@@ -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.
@@ -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`.
*
@@ -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]] {

0 comments on commit aef0473

Please sign in to comment.