Skip to content

Instantly share code, notes, and snippets.

@xdcrafts
Last active April 12, 2020 08:18
Show Gist options
  • Save xdcrafts/c6c662e164d22934cf1818f42281c057 to your computer and use it in GitHub Desktop.
Save xdcrafts/c6c662e164d22934cf1818f42281c057 to your computer and use it in GitHub Desktop.
import scala.concurrent.{Await, ExecutionContext, Future}
import scala.concurrent.duration.Duration
import scala.util.{Failure, Success, Try}
object app extends App {
import effect.Effect
import context._
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
private implicit val logger: Logger = LoggerFactory.getLogger(this.getClass)
final case class ExampleKey(value: String) extends ContextKey[String]
val key_1 = ExampleKey("key_1")
val key_2 = ExampleKey("key_2")
def log(message: String)(implicit logger: Logger): Effect[Unit] = for {
context <- Effect.context()
_ <- Effect.sync(logger.info(s"$message [$context]"))
} yield ()
final case class SomeBusinessValue(value: String = "") {
def nonEmpty: Boolean = value.nonEmpty
def capitalize: SomeBusinessValue = SomeBusinessValue(value.toUpperCase)
}
def businessFunction(): SomeBusinessValue = SomeBusinessValue()
val effect: Effect[SomeBusinessValue] = for {
_ <- Effect.set(key_1, "Before IF").updateOrFail[String](key_1, _.toUpperCase)
_ <- log { "Effect is not executed until calling run/await functions" }
_ <- log { "Example of a logging effect usage which has an access to a context" }
value <- Effect.sync { businessFunction() }
_ = value if value.nonEmpty
_ <- log { "Not reachable due to filtering in if expression" }
_ <- Effect.set(key_1, "After IF")
} yield value.capitalize
val recoveredEffect: Effect[SomeBusinessValue] =
for {
recovered <- effect.recoverWithF { case _ => Future(SomeBusinessValue("Recovered value")) }
_ <- Effect.set(key_2, "After Recover")
_ <- log("After recovering failed effect.")
} yield recovered
for {
str <- Future { "eager value" }
_ <- Future { logger.info("Future is executed when created") }
} yield str
logger.info(s"${recoveredEffect.await(10.seconds)}")
}
package object effect {
import context._
type EffectAlias[T] = Future[(Context, Try[T])]
type EffectFunction[T] = Context => EffectAlias[T]
final object EmptyEffectException extends RuntimeException
final class Effect[T](private val runF: EffectFunction[T]) {
def transform[U](fun: (Context, T) => (Context, U))(implicit ec: ExecutionContext): Effect[U] = Effect {
init: Context =>
runF.apply(init).map { case (ctx, result) =>
result match {
case Failure(thr) => ctx -> Failure(thr)
case Success(value) => Try(fun.apply(ctx, value)) match {
case Success((ctxU, valueU)) => ctxU -> Success(valueU)
case Failure(thrU) => ctx -> Failure(thrU)
}
}
}
}
def context()(implicit ec: ExecutionContext): Effect[Context] =
transform { case (ctx: Context, _: T) => ctx -> ctx }
def modify(fun: Context => Context)(implicit ec: ExecutionContext): Effect[T] =
transform { case (ctx: Context, t: T) => fun.apply(ctx) -> t }
def set[K](contextKey: ContextKey[K], value: K)(implicit ec: ExecutionContext): Effect[T] =
modify(_.set(contextKey, value))
def get[K](contextKey: ContextKey[K])(implicit ec: ExecutionContext): Effect[Option[K]] =
transform { case (ctx: Context, t: T) => ctx -> ctx.get(contextKey) }
def getOrElse[K](contextKey: ContextKey[K], default: K)(implicit ec: ExecutionContext): Effect[K] =
transform { case (ctx: Context, _: T) => ctx -> ctx.getOrElse(contextKey, default) }
def getOrFail[K](contextKey: ContextKey[K])(implicit ec: ExecutionContext): Effect[K] =
transform { case (ctx: Context, _: T) => ctx -> ctx.getOrFail(contextKey) }
def update[K](contextKey: ContextKey[K], fun: K => K)(implicit ec: ExecutionContext): Effect[T] =
modify(_.update(contextKey, fun))
def updateOrFail[K](contextKey: ContextKey[K], fun: K => K)(implicit ec: ExecutionContext): Effect[T] =
modify(_.updateOrFail(contextKey, fun))
def updateOrElse[K](contextKey: ContextKey[K], fun: K => K, default: K)
(implicit ec: ExecutionContext): Effect[T] =
modify(_.updateOrElse(contextKey, fun, default))
def map[U](fun: T => U)(implicit ec: ExecutionContext): Effect[U] =
transform((ctx: Context, t: T) => ctx -> fun.apply(t))
def flatMap[U](fun: T => Effect[U])(implicit ec: ExecutionContext): Effect[U] = Effect {
init: Context =>
runF.apply(init).flatMap { case (ctx, result) =>
result.flatMap(t => Try(fun.apply(t))) match {
case Failure(thr) => Future.successful(ctx -> Failure(thr))
case Success(eff) => eff.runF.apply(ctx)
}
}
}
def flatMapF[U](fun: T => Future[U])(implicit ec: ExecutionContext): Effect[U] = Effect {
init: Context =>
runF.apply(init).flatMap { case (ctx, result) =>
result.flatMap(t => Try(fun.apply(t))) match {
case Failure(thr) => Future.successful(ctx -> Failure(thr))
case Success(future) => future.map(Success(_)).recover { case thr => Failure(thr) }.map(ctx -> _)
}
}
}
def filter(predicate: T => Boolean)(implicit ec: ExecutionContext): Effect[T] = Effect {
init: Context =>
runF.apply(init).map { case (ctx, result) =>
result match {
case Failure(thr) => ctx -> Failure(thr)
case Success(value) =>
if (predicate.apply(value)) ctx -> Success(value)
else ctx -> Failure(EmptyEffectException)
}
}
}
def withFilter(predicate: T => Boolean)(implicit ec: ExecutionContext): Effect[T] = filter(predicate)
def recover(fun: PartialFunction[Throwable, T])(implicit ec: ExecutionContext): Effect[T] = Effect {
init: Context =>
runF.apply(init).map { case (ctx, result) =>
result match {
case Success(value) => ctx -> Success(value)
case Failure(thr) =>
if (fun.isDefinedAt(thr)) ctx -> Try(fun.apply(thr))
else ctx -> Failure(thr)
}
}
}
def recoverWith(fun: PartialFunction[Throwable, Effect[T]])(implicit ec: ExecutionContext): Effect[T] = Effect {
init: Context =>
runF.apply(init).flatMap { case (ctx, result) =>
result match {
case Success(value) => Future.successful(ctx -> Success(value))
case Failure(thr) =>
if (fun.isDefinedAt(thr)) Try(fun.apply(thr)) match {
case Failure(funThr) => Future.successful(ctx -> Failure(funThr))
case Success(eff) => eff.runF.apply(ctx)
}
else Future.successful(ctx -> Failure(thr))
}
}
}
def recoverWithF(fun: PartialFunction[Throwable, Future[T]])(implicit ec: ExecutionContext): Effect[T] = Effect {
init: Context =>
runF.apply(init).flatMap { case (ctx, result) =>
result match {
case Success(value) => Future.successful(ctx -> Success(value))
case Failure(thr) =>
if (fun.isDefinedAt(thr)) Try(fun.apply(thr)) match {
case Failure(funThr) => Future.successful(ctx -> Failure(funThr))
case Success(future) => future.map(Success(_)).recover { case futThr => Failure(futThr) }.map(ctx -> _)
}
else Future.successful(ctx -> Failure(thr))
}
}
}
def run(ctx: Context = Context()): Future[(Context, Try[T])] =
runF.apply(ctx)
def runC(ctx: Context = Context())(implicit ec: ExecutionContext): Future[(Context, T)] =
run(ctx).map {
case (_, Failure(thr)) => throw thr
case (ctx, Success(value)) => ctx -> value
}
def runF(ctx: Context = Context())(implicit ec: ExecutionContext): Future[T] =
run(ctx).map(_._2).map {
case Failure(thr) => throw thr
case Success(value) => value
}
def await(duration: Duration, ctx: Context = Context()): (Context, Try[T]) =
Await.result(run(ctx), duration)
def awaitC(duration: Duration, ctx: Context = Context())(implicit ec: ExecutionContext): (Context, T) =
Await.result(runC(ctx), duration)
def awaitF(duration: Duration, ctx: Context = Context())(implicit ec: ExecutionContext): T =
Await.result(runF(ctx), duration)
}
object Effect {
def apply[T](body: => EffectFunction[T]): Effect[T] = new Effect[T](runF = body)
@SuppressWarnings(Array("org.wartremover.warts.TryPartial"))
def applyF[T](body: => Future[T])
(implicit ec: ExecutionContext): Effect[T] = Effect {
ctx: Context =>
Try(body)
.map(_.map(Success(_)))
.recover { case throwable => Future.successful(Failure(throwable)) }
.get
.recover { case throwable => Failure(throwable) }
.map(ctx -> _)
}
def async[T](body: => T)
(implicit ec: ExecutionContext): Effect[T] = applyF(Future.apply(body))
@SuppressWarnings(Array("org.wartremover.warts.TryPartial"))
def sync[T](body: => T): Effect[T] = Try(body) match {
case Failure(thr) => failed(thr)
case Success(value) => successful(value)
}
def successful[T](value: T): Effect[T] = Effect {
ctx: Context => Future.successful(ctx -> Success(value))
}
def failed[T](throwable: Throwable): Effect[T] = Effect {
ctx: Context => Future.successful(ctx -> Failure(throwable))
}
def context(): Effect[Context] = Effect {
ctx: Context => Future.successful(ctx -> Success(ctx))
}
def get[T](contextKey: ContextKey[T]): Effect[Option[T]] = Effect {
ctx: Context => Future.successful(ctx -> Success(ctx.get(contextKey)))
}
def getOrElse[T](contextKey: ContextKey[T], default: T): Effect[T] = Effect {
ctx: Context => Future.successful(ctx -> Success(ctx.get(contextKey).getOrElse(default)))
}
def getOrFail[T](contextKey: ContextKey[T]): Effect[T] = Effect {
ctx: Context =>
ctx.get(contextKey) match {
case None => Future.successful(ctx -> Failure(MissingContextKeyException(s"$contextKey not found.")))
case Some(value) => Future.successful(ctx -> Success(value))
}
}
def set[T](contextKey: ContextKey[T], value: T): Effect[Context] = Effect {
ctx: Context =>
val nextCtx = ctx.set(contextKey, value)
Future.successful(nextCtx -> Success(nextCtx))
}
}
}
package object context {
final case class MissingContextKeyException(msg: String) extends RuntimeException(msg)
trait ContextKey[T]
trait Context {
def get[T](contextKey: ContextKey[T]): Option[T]
def getOrElse[T](contextKey: ContextKey[T], default: => T): T
def getOrFail[T](contextKey: ContextKey[T]): T =
get(contextKey) match {
case Some(value) => value
case None => throw MissingContextKeyException(s"$contextKey not found.")
}
def set[T](contextKey: ContextKey[T], value: => T): Context
def remove[T](contextKey: ContextKey[T]): Context
def update[T](contextKey: ContextKey[T], updateFn: T => T): Context
def updateOrElse[T](contextKey: ContextKey[T], updateFn: T => T, default: => T): Context
def updateOrFail[T](contextKey: ContextKey[T], updateFn: T => T): Context
}
object Context {
def apply(): Context = ImmutableContext(Map())
}
@SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf"))
private final case class ImmutableContext(private val map: Map[ContextKey[_], _]) extends Context {
def get[T](contextKey: ContextKey[T]): Option[T] =
map.get(contextKey).map(_.asInstanceOf[T])
def getOrElse[T](contextKey: ContextKey[T], default: => T): T =
get(contextKey).getOrElse(default)
def set[T](contextKey: ContextKey[T], value: => T): Context =
ImmutableContext(map + (contextKey -> value))
def remove[T](contextKey: ContextKey[T]): Context =
ImmutableContext(map - contextKey)
def update[T](contextKey: ContextKey[T], updateFn: T => T): Context =
get(contextKey) match {
case None => this
case Some(value) => set(contextKey, updateFn.apply(value))
}
def updateOrElse[T](contextKey: ContextKey[T], updateFn: T => T, default: => T): Context =
set(contextKey, get(contextKey).map(updateFn).getOrElse(default))
def updateOrFail[T](contextKey: ContextKey[T], updateFn: T => T): Context =
get(contextKey) match {
case None => throw MissingContextKeyException(s"$contextKey not found.")
case Some(value) => set(contextKey, updateFn.apply(value))
}
override def toString: String = s"Context#${map.toString()}"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment