All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jam.monad.Reval.scala Maven / Gradle / Ivy

The newest version!
package jam.monad

import cats._
import cats.effect._
import cats.implicits._
import jam.monad.core.RevalInstances
import jam.monad.core.RevalKeyGen

/**
 * `Reval` is a data structure which encodes the idea of allocating an object which has an
 * associated finalizer. Can be thought of as a mix of `cats.effect.Resource` and `cats.Eval`.
 * There are two allocation strategies:
 *   - `later`: allocates the object once when it is needed
 *   - `always`: allocates the object every time it is needed
 *
 * For example:
 * {{{
 * scala> var c = 0
 * scala> val a = Reval.thunkLater[IO, Int]{c += 1; c}
 * scala> a.replicateA(3).map(_.sum).usePure.unsafeRunSync() // List(1, 1, 1).sum
 * val res0: Int = 3
 * }}}
 * {{{
 * scala> var c = 0
 * scala> val a = Reval.thunkAlways[IO, Int]{c += 1; c}
 * scala> a.replicateA(3).map(_.sum).usePure.unsafeRunSync() // List(1, 2, 3).sum
 * val res0: Int = 6
 * }}}
 *
 * Common pitfalls:
 *   - `later` with multiple `use` calls will allocate later objects for each `use`
 * {{{
 * scala> var c = 0
 * scala> val a = Reval.thunkLater[IO, Int] { c += 1; c }
 * scala> (a.usePure, a.usePure).mapN(_ + _).unsafeRunSync() // 1 + 2
 * val res1: Int = 3
 * }}}
 */
sealed abstract class Reval[F[_], +A] {

    /**
     * Allocates an object and supplies it to the given function. The finalizer is called as
     * soon as the resulting `F[B]` is completed, whether normally or as a raised error.
     */
    def use[B](f: A => F[B])(implicit F: MonadCancel[F, Throwable]): F[B] = asResource.use(f)

    def usePure[B](implicit ev: A <:< B, F: MonadCancel[F, Throwable]): F[B] = use(F.pure(_))

    /**
     * Represents a reval as a `cats.effect.Resource`.
     */
    def asResource: Resource[F, A] = {
        def loop[C](
            reval: Reval[F, C],
            revalMap: Map[Object, Resource[F, Any]],
        ): Resource[F, (Resource[F, C], Map[Object, Resource[F, Any]])] = reval match {
            case self: Reval.Memoize[F, C] =>
                revalMap.get(self.key).fold(
                    for {
                        result <- loop(self.s, revalMap)
                        a <- result._1
                        r = Resource.pure[F, C](a)
                    } yield r -> (result._2 + (self.key -> r))
                )(r => (r.asInstanceOf[Resource[F, C]], revalMap).pure[Resource[F, *]])
            case self: Reval.Eval[F, C] => (self.r, revalMap).pure[Resource[F, *]]
            case self: Reval.FlatMap[F, Any, C] =>
                for {
                    aResult <- loop(self.s, revalMap)
                    a <- aResult._1
                    fb = self.fs(a)
                    bResult <- loop(fb, aResult._2)
                } yield bResult
        }
        loop(this, Map.empty).flatMap(_._1)
    }

    def flatMap[B](f: A => Reval[F, B]): Reval[F, B] = Reval.FlatMap(this, f)

    def map[B](f: A => B): Reval[F, B] = flatMap(a => Reval.pure(f(a)))

    def combine[B >: A: Semigroup](that: Reval[F, B]): Reval[F, B] = for {
        a <- this
        b <- that
    } yield Semigroup[B].combine(a, b)

    def handleErrorWith[B >: A, E](f: E => Reval[F, B])(implicit
        E: ApplicativeError[F, E]
    ): Reval[F, B] = attempt.flatMap {
        case Right(a) => Reval.pure(a)
        case Left(e) => f(e)
    }

    def attempt[E](implicit F: ApplicativeError[F, E]): Reval[F, Either[E, A]] = this match {
        case self: Reval.Eval[F, A] => Reval.Eval(self.r.attempt)
        case self: Reval.Memoize[F, A] => Reval.Memoize(self.s.attempt, self.key)
        case self: Reval.FlatMap[F, Any, A] =>
            self.s.attempt.flatMap {
                case Left(error) => Reval.pure(Either.left[E, A](error))
                case Right(a) => self.fs(a).attempt
            }
    }

    /**
     * Evaluates `f` before the object is allocated.
     */
    def preAllocate(f: F[Unit]): Reval[F, A] = this match {
        case self: Reval.Memoize[F, A] => Reval.Memoize(self.s.preAllocate(f), self.key)
        case _ => Reval.evalAlways(f).flatMap(_ => this)
    }

    def thunkPreAllocate(f: => Unit)(implicit F: Sync[F]): Reval[F, A] =
        preAllocate(F.delay(f))

    /**
     * Evaluates `f` after the object finalizer is called.
     */
    def postFinalize(f: F[Unit])(implicit F: Applicative[F]): Reval[F, A] = this match {
        case self: Reval.Memoize[F, A] => Reval.Memoize(self.s.postFinalize(f), self.key)
        case _ => Reval.always(F.unit.map(_ -> f)).flatMap(_ => this)
    }

    def thunkPostFinalize(f: => Unit)(implicit F: Sync[F]): Reval[F, A] =
        postFinalize(F.delay(f))

    /**
     * Ensures that the object will be allocated once.
     */
    def memoize[B](implicit ev: A <:< B, B: RevalKeyGen[B]): Reval[F, B] =
        Reval.Memoize(this.map(ev), B.key)
}

object Reval extends RevalInstances {

    /**
     * Constructs `Reval` from allocation and release functions
     * that will be executed once when it is needed.
     */
    def later[F[_]: Functor, A: RevalKeyGen](reval: F[(A, F[Unit])]): Reval[F, A] =
        resourceLater(Resource.apply(reval))

    def makeLater[F[_]: Functor, A: RevalKeyGen](allocate: F[A])(release: A => F[Unit]): Reval[F, A] =
        later(allocate.map(a => a -> release(a)))

    def makeThunkLater[F[_]: Sync, A: RevalKeyGen](allocate: => A)(release: A => Unit): Reval[F, A] =
        later(Sync[F].delay(allocate).map(a => a -> Sync[F].delay(release(a))))

    def resourceLater[F[_], A: RevalKeyGen](r: Resource[F, A]): Reval[F, A] =
        resourceAlways(r).memoize

    /**
     * Constructs `Reval` from an allocation function with no-op release
     * that will be executed once when it is needed.
     */
    def evalLater[F[_], A: RevalKeyGen](f: F[A]): Reval[F, A] =
        resourceLater(Resource.eval(f))

    def thunkLater[F[_]: Sync, A: RevalKeyGen](a: => A): Reval[F, A] =
        evalLater(Sync[F].delay(a))

    /**
     * Constructs `Reval` from allocation and release functions
     * that will be executed always when it is needed.
     */
    def always[F[_]: Functor, A](reval: F[(A, F[Unit])]): Reval[F, A] =
        resourceAlways(Resource.apply(reval))

    def makeAlways[F[_]: Functor, A](allocate: F[A])(release: A => F[Unit]): Reval[F, A] =
        always(allocate.map(a => a -> release(a)))

    def makeThunkAlways[F[_]: Sync, A](allocate: => A)(release: A => Unit): Reval[F, A] =
        always(Sync[F].delay(allocate).map(a => a -> Sync[F].delay(release(a))))

    def resourceAlways[F[_], A](r: Resource[F, A]): Reval[F, A] = Eval(r)

    /**
     * Constructs `Reval` from an allocation function with no-op release
     * that will be executed always when it is needed.
     */
    def evalAlways[F[_], A](f: F[A]): Reval[F, A] = resourceAlways(Resource.eval(f))

    def thunkAlways[F[_]: Sync, A](a: => A): Reval[F, A] = evalAlways(Sync[F].delay(a))

    /**
     * Constructs `Reval` from a pure value with no-op release.
     */
    def pure[F[_], A](a: A): Reval[F, A] = Eval(Resource.pure(a))

    def raiseError[F[_], A, E](e: E)(implicit E: ApplicativeError[F, E]): Reval[F, A] =
        Reval.evalAlways(E.raiseError[A](e))

    final case class FlatMap[F[_], A, +B](s: Reval[F, A], fs: A => Reval[F, B]) extends Reval[F, B]

    final case class Eval[F[_], +A](r: Resource[F, A]) extends Reval[F, A]

    final case class Memoize[F[_], +A](s: Reval[F, A], key: Object) extends Reval[F, A]
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy