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

io.catbird.util.Rerunnable.scala Maven / Gradle / Ivy

The newest version!
package io.catbird.util

import cats.{
  Applicative,
  CoflatMap,
  CommutativeApplicative,
  Comonad,
  Eq,
  Monad,
  MonadError,
  Monoid,
  Parallel,
  Semigroup,
  ~>
}
import com.twitter.util.{ Await, Duration, Future, FuturePool, Return, Throw, Try }
import java.lang.Throwable

import scala.{ Boolean, Unit }
import scala.annotation.tailrec
import scala.util.{ Either, Left, Right }

abstract class Rerunnable[+A] { self =>
  def run: Future[A]

  final def map[B](f: A => B): Rerunnable[B] = new Rerunnable.Bind[B] {
    type P = A

    final def fa: Rerunnable[A] = self
    final def ff: Try[A] => Rerunnable[B] = {
      case Return(a)    => Rerunnable.const[B](f(a))
      case Throw(error) => Rerunnable.raiseError[B](error)
    }
  }

  final def flatMap[B](f: A => Rerunnable[B]): Rerunnable[B] = new Rerunnable.Bind[B] {
    type P = A

    final def fa: Rerunnable[A] = self
    final def ff: Try[A] => Rerunnable[B] = {
      case Return(a)    => f(a)
      case Throw(error) => Rerunnable.raiseError[B](error)
    }
  }

  final def flatMapF[B](f: A => Future[B]): Rerunnable[B] = new Rerunnable[B] {
    final def run: Future[B] = self.run.flatMap(f)
  }

  final def product[B](other: Rerunnable[B]): Rerunnable[(A, B)] = new Rerunnable.Bind[(A, B)] {
    type P = A

    final def fa: Rerunnable[A] = self
    final def ff: Try[A] => Rerunnable[(A, B)] = {
      case Return(a) =>
        new Rerunnable.Bind[(A, B)] {
          type P = B

          final def fa: Rerunnable[B] = other
          final def ff: Try[B] => Rerunnable[(A, B)] = {
            case Return(b)    => Rerunnable.const((a, b))
            case Throw(error) => Rerunnable.raiseError[(A, B)](error)
          }
        }
      case Throw(error) => Rerunnable.raiseError[(A, B)](error)
    }
  }

  final def liftToTry: Rerunnable[Try[A]] = new Rerunnable.Bind[Try[A]] {
    type P = A
    final def fa: Rerunnable[A] = self
    final def ff: Try[A] => Rerunnable[Try[A]] = Rerunnable.const(_)
  }
}

final object Rerunnable extends RerunnableInstances1 {
  @tailrec
  private[this] def reassociate[B](bind: Bind[B]): Bind[B] =
    if (bind.fa.isInstanceOf[Bind[_]]) {
      val inner = bind.fa.asInstanceOf[Bind[bind.P]]
      val next = new Bind[B] {
        final type P = inner.P

        final def fa: Rerunnable[P] = inner.fa
        final def ff: Try[P] => Rerunnable[B] = p =>
          new Bind[B] {
            final type P = bind.P

            final val fa: Rerunnable[P] = inner.ff(p)
            final val ff: Try[P] => Rerunnable[B] = bind.ff
          }
      }

      reassociate(next)
    } else bind

  private[util] abstract class Bind[B] extends Rerunnable[B] { self =>
    type P

    def fa: Rerunnable[P]
    def ff: Try[P] => Rerunnable[B]

    private[this] lazy val next = reassociate[B](this)
    final def run: Future[B] = {
      val localff = next.ff // don't close over this or next in closure
      next.fa.run.transform(t => localff(t).run)
    }
  }

  private[util] case class ConstRerunnable[A](run: Future[A]) extends Rerunnable[A]

  final def const[A](a: A): Rerunnable[A] =
    ConstRerunnable(Future.value(a))

  final def raiseError[A](error: Throwable): Rerunnable[A] =
    ConstRerunnable(Future.exception[A](error))

  final def apply[A](a: => A): Rerunnable[A] = new Rerunnable[A] {
    final def run: Future[A] = Future(a)
  }

  final def suspend[A](fa: => Rerunnable[A]): Rerunnable[A] = new Rerunnable[A] {
    final def run: Future[A] = Future(fa).flatMap(_.run)
  }

  final def fromFuture[A](fa: => Future[A]): Rerunnable[A] = new Rerunnable[A] {
    final def run: Future[A] = fa
  }

  final def withFuturePool[A](pool: FuturePool)(a: => A): Rerunnable[A] = new Rerunnable[A] {
    final def run: Future[A] = pool(a)
  }

  final val Unit: Rerunnable[Unit] =
    ConstRerunnable(Future.Unit)

  implicit final val rerunnableInstance: MonadError[Rerunnable, Throwable] with CoflatMap[Rerunnable] =
    new RerunnableMonadError with RerunnableCoflatMap

  implicit final def rerunnableMonoid[A](implicit A: Monoid[A]): Monoid[Rerunnable[A]] =
    new RerunnableSemigroup[A] with Monoid[Rerunnable[A]] {
      final val empty: Rerunnable[A] = Rerunnable.rerunnableInstance.pure(A.empty)
    }

  implicit final val rerunnableParallelInstance: Parallel.Aux[Rerunnable, Rerunnable.Par] =
    new Parallel[Rerunnable] {
      type F[x] = Rerunnable.Par[x]

      final override val applicative: Applicative[Rerunnable.Par] =
        rerunnableParCommutativeApplicative

      final override val monad: Monad[Rerunnable] =
        rerunnableInstance

      final override val sequential: Rerunnable.Par ~> Rerunnable =
        λ[Rerunnable.Par ~> Rerunnable](Rerunnable.Par.unwrap(_))

      final override val parallel: Rerunnable ~> Rerunnable.Par =
        λ[Rerunnable ~> Rerunnable.Par](Rerunnable.Par(_))
    }

  /**
   * Obtain a [[cats.Eq]] instance for [[Rerunnable]].
   *
   * This version is only useful for successful actions: if one fails, the resulting exception will be thrown.
   *
   * These instances use [[com.twitter.util.Await]] so should be
   * [[https://finagle.github.io/blog/2016/09/01/block-party/ avoided in production code]].  Likely use cases
   * include tests, scrips, REPLs etc.
   */
  final def rerunnableEq[A](atMost: Duration)(implicit A: Eq[A]): Eq[Rerunnable[A]] =
    Eq.by[Rerunnable[A], Future[A]](_.run)(futureEq[A](atMost))

  /**
   * Obtain a [[cats.Eq]] instance for [[Rerunnable]].
   *
   * This version can also compare failed actions and thus requires an `Eq[Throwable]` in scope.
   *
   * These instances use [[com.twitter.util.Await]] so should be
   * [[https://finagle.github.io/blog/2016/09/01/block-party/ avoided in production code]].  Likely use cases
   * include tests, scrips, REPLs etc.
   */
  final def rerunnableEqWithFailure[A](atMost: Duration)(implicit A: Eq[A], T: Eq[Throwable]): Eq[Rerunnable[A]] =
    Eq.by[Rerunnable[A], Future[A]](_.run)(futureEqWithFailure[A](atMost))

  /**
   * FunctionK instance to convert to Future.
   *
   * Useful when you need a parameter for a `mapK` method, for example you could use
   * [[https://typelevel.org/cats-tagless/ cats-tagless]] in a codebase that mixes [[Rerunnable]] and
   * [[Future]].
   */
  final val toFuture: Rerunnable ~> Future = λ[Rerunnable ~> Future](_.run)
}

private[util] trait RerunnableInstances1 extends RerunnableParallelNewtype {

  /**
   * Obtain a [[cats.Comonad]] instance for [[Rerunnable]]
   *
   * These instances use [[com.twitter.util.Await]] so should be
   * [[https://finagle.github.io/blog/2016/09/01/block-party/ avoided in production code]].  Likely use cases
   * include tests, scrips, REPLs etc.
   */
  final def rerunnableComonad(atMost: Duration): Comonad[Rerunnable] =
    new RerunnableCoflatMap with Comonad[Rerunnable] {
      final def extract[A](x: Rerunnable[A]): A = Await.result(x.run, atMost)
      final def map[A, B](fa: Rerunnable[A])(f: A => B): Rerunnable[B] = fa.map(f)
    }

  implicit def rerunnableSemigroup[A](implicit A: Semigroup[A]): Semigroup[Rerunnable[A]] =
    new RerunnableSemigroup[A]
}

private[util] trait RerunnableParallelNewtype {
  type Par[+A] = Par.Type[A]

  object Par {
    type Type[+A] = Rerunnable[A]

    def apply[A](fa: Rerunnable[A]): Type[A] =
      fa.asInstanceOf[Type[A]]

    def unwrap[A](fa: Type[A]): Rerunnable[A] =
      fa.asInstanceOf[Rerunnable[A]]
  }

  implicit final val rerunnableParCommutativeApplicative: CommutativeApplicative[Par] =
    new CommutativeApplicative[Par] {
      import Par.unwrap

      final override def pure[A](x: A): Rerunnable.Par[A] =
        Par(Rerunnable.const(x))
      final override def map2[A, B, Z](
        fa: Rerunnable.Par[A],
        fb: Rerunnable.Par[B]
      )(f: (A, B) => Z): Rerunnable.Par[Z] =
        // Future.join runs futures in parallel
        Par(Rerunnable.fromFuture(Future.join(unwrap(fa).run, unwrap(fb).run)).map(f.tupled))
      final override def ap[A, B](ff: Rerunnable.Par[A => B])(fa: Rerunnable.Par[A]): Rerunnable.Par[B] =
        map2(ff, fa)(_(_))
      final override def product[A, B](fa: Rerunnable.Par[A], fb: Rerunnable.Par[B]): Rerunnable.Par[(A, B)] =
        map2(fa, fb)((_, _))
      final override def map[A, B](fa: Rerunnable.Par[A])(f: A => B): Rerunnable.Par[B] =
        Par(unwrap(fa).map(f))
      final override def unit: Rerunnable.Par[scala.Unit] =
        Par(Rerunnable.Unit)
    }

  final def rerunnableParEq[A](atMost: Duration)(implicit A: Eq[A]): Eq[Par[A]] = new Eq[Par[A]] {
    import Par.unwrap

    final override def eqv(x: Par[A], y: Par[A]): Boolean =
      Rerunnable.rerunnableEq(atMost).eqv(unwrap(x), unwrap(y))
  }

  final def rerunnableParEqWithFailure[A](atMost: Duration)(implicit A: Eq[A], T: Eq[Throwable]): Eq[Par[A]] =
    new Eq[Par[A]] {
      import Par.unwrap

      override def eqv(x: Par[A], y: Par[A]): Boolean =
        Rerunnable.rerunnableEqWithFailure(atMost)(A, T).eqv(unwrap(x), unwrap(y))
    }
}

private[util] sealed trait RerunnableCoflatMap extends CoflatMap[Rerunnable] {
  final def coflatMap[A, B](fa: Rerunnable[A])(f: Rerunnable[A] => B): Rerunnable[B] =
    new Rerunnable[B] {
      final def run: Future[B] = Future(f(fa))
    }
}

private[util] class RerunnableMonadError extends MonadError[Rerunnable, Throwable] {
  final def pure[A](a: A): Rerunnable[A] = Rerunnable.const(a)

  override final def map[A, B](fa: Rerunnable[A])(f: A => B): Rerunnable[B] = fa.map(f)
  override final def product[A, B](fa: Rerunnable[A], fb: Rerunnable[B]): Rerunnable[(A, B)] = fa.product(fb)

  final def flatMap[A, B](fa: Rerunnable[A])(f: A => Rerunnable[B]): Rerunnable[B] = fa.flatMap(f)

  final def raiseError[A](e: Throwable): Rerunnable[A] = Rerunnable.raiseError(e)

  final def handleErrorWith[A](fa: Rerunnable[A])(f: Throwable => Rerunnable[A]): Rerunnable[A] = new Rerunnable[A] {
    final def run: Future[A] = fa.run.rescue { case error =>
      f(error).run
    }
  }

  override final def attempt[A](fa: Rerunnable[A]): Rerunnable[Either[Throwable, A]] = fa.liftToTry.map {
    case Return(a)  => Right[Throwable, A](a)
    case Throw(err) => Left[Throwable, A](err)
  }

  final def tailRecM[A, B](a: A)(f: A => Rerunnable[Either[A, B]]): Rerunnable[B] = f(a).flatMap {
    case Right(b)    => pure(b)
    case Left(nextA) => tailRecM(nextA)(f)
  }
}

private[util] sealed class RerunnableSemigroup[A](implicit A: Semigroup[A]) extends Semigroup[Rerunnable[A]] {
  final def combine(fx: Rerunnable[A], fy: Rerunnable[A]): Rerunnable[A] = fx.product(fy).map { case (x, y) =>
    A.combine(x, y)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy