Created
November 2, 2018 13:17
-
-
Save Daenyth/349ef64c0e8afb79f77b1ae420ffafaa to your computer and use it in GitHub Desktop.
RetriableT.scala
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package teikametrics.sync | |
import scala.concurrent.{ExecutionContext, Future} | |
sealed trait Retriable[+T] { | |
def toOption: Option[T] | |
} | |
case class Ready[T](result: T) extends Retriable[T] { | |
override val toOption = Some(result) | |
} | |
object Retry extends Retriable[Nothing] { | |
override val toOption = None | |
def apply[T]: Retriable[T] = this | |
} | |
object Retriable { | |
def recoverAndRetry[T](canRetry: Exception => Boolean)(effect: => Future[T])( | |
implicit ec: ExecutionContext): Future[Retriable[T]] = | |
effect | |
.map(Ready(_)) | |
.recover { | |
case exception: Exception if canRetry(exception) => | |
Retry | |
} | |
def fromOption[T](option: Option[T]): Retriable[T] = | |
option.map(Ready(_)).getOrElse(Retry) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package teikametrics.sync | |
import cats.effect.{IO, LiftIO, Sync} | |
import cats.{ | |
Applicative, | |
ApplicativeError, | |
Functor, | |
Monad, | |
MonadError, | |
StackSafeMonad, | |
~> | |
} | |
import scala.language.higherKinds | |
/** A monad transformer that enhances `F` with the ability to short-circuit out | |
* of map/flatMap with a `Retry` instruction in addition to normal flatMap on `A` | |
*/ | |
case class RetriableT[F[_], A](value: F[Retriable[A]]) { | |
def mapK[G[_]](f: F ~> G): RetriableT[G, A] = RetriableT[G](f(value)) | |
// Defined here on the class in addition to the instance because with only the typeclass method, | |
// intellij always thinks `myRetriableTObj.flatMap` is a compile error and red-marks it. | |
// It also allows you to skip an `import cats.implicits._` | |
def flatMap[B](f: A => RetriableT[F, B])( | |
implicit F: Monad[F]): RetriableT[F, B] = | |
Monad[RetriableT[F, ?]].flatMap(this)(f) | |
// As with flatMap, this is defined just so that intellij makes fewer red marks | |
def map[B](f: A => B)(implicit F: Monad[F]): RetriableT[F, B] = | |
Monad[RetriableT[F, ?]].map(this)(f) | |
// As with flatMap, this is defined just so that intellij makes fewer red marks | |
def *>[B](fb: RetriableT[F, B])(implicit F: Monad[F]): RetriableT[F, B] = | |
Monad[RetriableT[F, ?]].productR(this)(fb) | |
// As with flatMap, this is defined just so that intellij makes fewer red marks | |
def void(implicit F: Monad[F]): RetriableT[F, Unit] = map(_ => ()) | |
// As with flatMap, this is defined just so that intellij makes fewer red marks | |
def as[B](b: B)(implicit F: Monad[F]): RetriableT[F, B] = map(_ => b) | |
} | |
object RetriableT extends RetriableTInstances { | |
def apply[F[_]] = new RetriableTPartiallyApplied[F] | |
/** Alias for ready() under a more common name */ | |
def liftF[F[_], A](value: F[A])( | |
implicit functor: Functor[F]): RetriableT[F, A] = | |
ready(value) | |
def ready[F[_], A](value: F[A])( | |
implicit functor: Functor[F]): RetriableT[F, A] = | |
RetriableT(functor.map(value)(Ready(_))) | |
def retry[F[_], A](implicit app: Applicative[F]): RetriableT[F, A] = | |
RetriableT(app.pure(Retry[A])) | |
def fromRetriable[F[_], A](value: Retriable[A])( | |
implicit app: Applicative[F]): RetriableT[F, A] = | |
RetriableT(app.pure(value)) | |
def fromOption[F[_]: Applicative, A](option: Option[A]): RetriableT[F, A] = | |
fromRetriable(Retriable.fromOption(option)) | |
/** A natural translation from `F` to `RetriableT[F, ?]`, ie "forall A: F[A] => RetriableT[F, A]" */ | |
def liftK[F[_]: Functor]: F ~> RetriableT[F, ?] = | |
λ[F ~> RetriableT[F, ?]](RetriableT[F].ready(_)) | |
} | |
class RetriableTPartiallyApplied[F[_]] { | |
def pure[A](value: A)(implicit F: Applicative[F]): RetriableT[F, A] = | |
RetriableT(F.pure(Ready(value))) | |
def apply[A](value: F[Retriable[A]]): RetriableT[F, A] = | |
RetriableT[F, A](value) | |
def ready[A](value: F[A])(implicit functor: Functor[F]): RetriableT[F, A] = | |
RetriableT.ready[F, A](value) | |
def retry[A](implicit app: Applicative[F]): RetriableT[F, A] = | |
RetriableT.retry[F, A] | |
def fromRetriable[A](value: Retriable[A])( | |
implicit app: Applicative[F]): RetriableT[F, A] = | |
RetriableT.fromRetriable[F, A](value) | |
def fromOption[A](option: Option[A])( | |
implicit app: Applicative[F]): RetriableT[F, A] = | |
RetriableT.fromOption[F, A](option) | |
def raiseError[A](err: Throwable)( | |
implicit F: ApplicativeError[F, Throwable] | |
): RetriableT[F, A] = | |
RetriableT.ready(F.raiseError(err)) | |
def delay[A](thunk: => A)(implicit F: Sync[F]): RetriableT[F, A] = | |
RetriableT.ready(F.delay(thunk)) | |
} | |
private[sync] sealed abstract class RetriableTInstances | |
extends RetriableTInstancesPriority0 { | |
implicit def monad[M[_]](implicit m: Monad[M]): Monad[RetriableT[M, ?]] = | |
new RetriableTMonad[M] { implicit val monad: Monad[M] = m } | |
implicit def liftIO[F[_]: Functor]( | |
implicit F: LiftIO[F]): LiftIO[RetriableT[F, ?]] = | |
new LiftIO[RetriableT[F, ?]] { | |
override def liftIO[A](ioa: IO[A]): RetriableT[F, A] = | |
RetriableT.ready(F.liftIO(ioa)) | |
} | |
} | |
// The "priority" traits are a standard trick for when you have scala typeclasses which overlap with each other. | |
// Sync[F] is a subclass of MonadError[F, Throwable], so if the Sync instance is defined in the same class as | |
// the MonadError definition, scala gives both of them the same "score" during implicit resolution, | |
// and it can't select between them, so it just says it can't find the implicit instance when you want | |
// to use it. | |
private[sync] sealed trait RetriableTInstancesPriority0 | |
extends RetriableTInstancesPriority1 { | |
implicit def monadError[M[_], E]( | |
implicit m: MonadError[M, E]): MonadError[RetriableT[M, ?], E] = | |
new RetriableTMonadError[M, E] { implicit val monad = m } | |
} | |
private[sync] sealed trait RetriableTInstancesPriority1 { | |
implicit def syncForRetriable[F[_]]( | |
implicit F: Sync[F]): Sync[RetriableT[F, ?]] = | |
new RetriableTSync[F] { implicit val monad: Sync[F] = F } | |
} | |
trait RetriableTMonad[M[_]] extends StackSafeMonad[RetriableT[M, ?]] { | |
implicit def monad: Monad[M] | |
def pure[A](x: A): RetriableT[M, A] = RetriableT(monad.pure(Ready(x))) | |
def flatMap[A, B](fa: RetriableT[M, A])( | |
f: A => RetriableT[M, B]): RetriableT[M, B] = | |
RetriableT( | |
monad.flatMap(fa.value) { | |
case Ready(a) => f(a).value | |
case Retry => monad.pure(Retry) | |
} | |
) | |
} | |
trait RetriableTMonadError[M[_], E] | |
extends MonadError[RetriableT[M, ?], E] with RetriableTMonad[M] { | |
override def monad: MonadError[M, E] | |
def handleErrorWith[A](fa: RetriableT[M, A])( | |
f: E => RetriableT[M, A]): RetriableT[M, A] = | |
RetriableT(monad.handleErrorWith(fa.value) { e => | |
f(e).value | |
}) | |
def raiseError[A](e: E): RetriableT[M, A] = | |
RetriableT(monad.raiseError(e)) | |
} | |
trait RetriableTSync[F[_]] | |
extends RetriableTMonadError[F, Throwable] with Sync[RetriableT[F, ?]] { | |
override def monad: Sync[F] | |
override def suspend[A](thunk: => RetriableT[F, A]): RetriableT[F, A] = | |
flatten(RetriableT.ready(monad.delay(thunk))(monad)) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment