diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 25cc9ae..7bf7677 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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' diff --git a/build.sbt b/build.sbt index cdd7937..d4ff504 100644 --- a/build.sbt +++ b/build.sbt @@ -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) diff --git a/src/main/scala/pl/iterators/sealedmonad/Sealed.scala b/src/main/scala/pl/iterators/sealedmonad/Sealed.scala index cbe7643..dc508d2 100644 --- a/src/main/scala/pl/iterators/sealedmonad/Sealed.scala +++ b/src/main/scala/pl/iterators/sealedmonad/Sealed.scala @@ -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,62 +418,31 @@ 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)) @@ -483,24 +450,86 @@ object Sealed extends SealedInstances { 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]] {