Last active
April 12, 2020 08:18
-
-
Save xdcrafts/c6c662e164d22934cf1818f42281c057 to your computer and use it in GitHub Desktop.
This file contains 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
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