Skip to content

Instantly share code, notes, and snippets.

@Daenyth
Created November 2, 2018 13:17
Show Gist options
  • Save Daenyth/349ef64c0e8afb79f77b1ae420ffafaa to your computer and use it in GitHub Desktop.
Save Daenyth/349ef64c0e8afb79f77b1ae420ffafaa to your computer and use it in GitHub Desktop.
RetriableT.scala
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)
}
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