-
Notifications
You must be signed in to change notification settings - Fork 89
Guarded effect with Guard API #1403
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
b303552
94af96f
40909ec
7c8dbbd
2b4d833
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| package kyo | ||
|
|
||
| import kyo.Guard.CanGuard | ||
| import kyo.kernel.ArrowEffect | ||
|
|
||
| /** Protect effect(s) [[S]] from being handled. To be used in functions that accept effects with generic effect types in order to avoid | ||
| * accidentally handling them. Can only be used within [[Guard.use]] blocks. | ||
| * | ||
| * @tparam S | ||
| * The effect type that is guarded | ||
| * @see | ||
| * [[Guard]] | ||
| */ | ||
| sealed trait Guarded[S] extends ArrowEffect[[A] =>> A < S, Id] | ||
|
|
||
| /** Utility for protecting any effect of type [[S]] from being handled | ||
| * | ||
| * @tparam S | ||
| * The effect type that can be guarded | ||
| */ | ||
| sealed class Guard[S](using tag: Tag[Guarded[S]]): | ||
|
||
|
|
||
| /** Guard an effect from being handled, wrapping its effect type [[S]] with [[Guarded]] | ||
| * | ||
| * @param effect | ||
| * The effect to be guarded | ||
| * @return | ||
| * A new effect with its effect intersection protected from handling | ||
| */ | ||
| def apply[A](effect: A < S)(using CanGuard, Frame): A < Guarded[S] = | ||
| ArrowEffect.suspend[A](tag, effect) | ||
|
||
| end Guard | ||
|
|
||
| object Guard: | ||
| sealed abstract class CanGuard | ||
| private val canGuardInstance = new CanGuard {} | ||
|
|
||
| private def run[S](using Frame)[A, S1](effect: A < (Guarded[S] & S1))(using tag: Tag[Guarded[S]]): A < (S & S1) = | ||
| ArrowEffect.handle(tag, effect)( | ||
| handle = [C] => (input, cont) => input.map(c => cont(c)) | ||
| ) | ||
|
|
||
| /** Use a [[Guard]] instance to protect one or more effects with effect type [[S]] from being handled within the scope of a provided | ||
| * function by lifting them to [[Guarded]][S]. To be used in functions that accept effects with generic effect types in order to avoid | ||
| * accidentally handling them. The guarded type [[Guarded]][S] is handled to [[S]] at the end of the scope. | ||
| * | ||
| * Example: | ||
| * | ||
| * def genericFunction[S](effect: Int < S): Int < S = Guard.use: guard => Abort.fold(identity, _ => 0): // Converts failure to 0 | ||
| * guard(Kyo.zip(effect, effect)).map: case (i, j) => val result = i + j if result < 0 then Abort.fail("negative!") else i + j | ||
| * | ||
| * Abort.run[String](genericFunction(Abort.fail("failed!"))).eval // Result: Result.Failure("failed!") <-- failure is not converted to | ||
| * 0 | ||
| * | ||
| * @tparam S | ||
| * Effect type intersection of the effects to be guarded within the scope of [[f]] | ||
| * @param f | ||
| * Function that uses a [[Guard]][S] instance to protect one or more effects from being handled | ||
| * @return | ||
| * An effect with the original guarded effect(s) [[S]] along with any additional effects [[S1]] accumulated when using them | ||
| */ | ||
| def use[S](using Tag[Guarded[S]], Frame)[A, S1](f: CanGuard ?=> Guard[S] => A < (Guarded[S] & S1)): A < (S & S1) = | ||
| given CanGuard = canGuardInstance | ||
| val guard = Guard[S] | ||
| val result = f(guard) | ||
| Guard.run[S](result) | ||
| end use | ||
| end Guard | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| package kyo | ||
|
|
||
| class GuardTest extends Test: | ||
|
||
|
|
||
| "unprotected arrow effect" in { | ||
| def genericFn[S](intEffect: Int < S)(using Tag[S]): Int < S = | ||
| Var.run(2): | ||
| Var.get[Int].map: i => | ||
| intEffect.map: j => | ||
| i + j | ||
|
|
||
| val eff = Var.update[Int](_ + 1) | ||
|
|
||
| assert(Var.run(10)(genericFn(eff)).eval == 5) | ||
| } | ||
|
|
||
| "protects arrow effect" in { | ||
| def genericFn[S](intEffect: Int < S)(using Tag[S]): Int < S = | ||
| Guard.use[S]: guard => | ||
| Var.run(2): | ||
| Var.get[Int].map: i => | ||
| guard(intEffect).map: j => | ||
| i + j | ||
|
|
||
| val eff = Var.update[Int](_ + 1) | ||
|
|
||
| assert(Var.run(10)(genericFn(eff)).eval == 13) | ||
| } | ||
|
|
||
| "unprotected context effect" in { | ||
| def genericFn[S](intEffect: Int < S)(using Tag[S]): Int < S = | ||
| Env.run(5): | ||
| Env.get[Int].map: i => | ||
| intEffect.map: j => | ||
| i + j | ||
|
|
||
| val eff = Env.get[Int] | ||
|
|
||
| assert(Env.run(10)(genericFn(eff)).eval == 10) | ||
| } | ||
|
|
||
| "guard protects context effect" in { | ||
| def genericFn[S](intEffect: Int < S)(using Tag[S]): Int < S = | ||
| Guard.use[S]: guard => | ||
| Env.run(5): | ||
| Env.get[Int].map: i => | ||
| guard(intEffect).map: j => | ||
| i + j | ||
|
|
||
| val eff = Env.get[Int] | ||
|
|
||
| assert(Env.run(10)(genericFn(eff)).eval == 15) | ||
| } | ||
|
|
||
| "unprotected complex effect" in { | ||
| def genericFn[S](intEffect: Int < S)(using Tag[S]): Int < S = | ||
| Env.run(5): | ||
| Env.get[Int].map: i => // 5 | ||
| intEffect.map: j => // 25 | ||
| Var.run(2): | ||
| Var.get[Int].map: k => // 2 | ||
| intEffect.map: l => // 7 | ||
| i + j + k + l | ||
|
|
||
| val eff = Env.get[Int].map(i => Var.get[Int].map(j => i + j)) | ||
|
|
||
| assert(Env.run(10)(Var.run(20)(genericFn(eff))).eval == 39) | ||
| } | ||
|
|
||
| "guard protects complex effect" in { | ||
| def genericFn[S](intEffect: Int < S)(using Tag[S]): Int < S = | ||
| Guard.use[S]: guard => | ||
| val guardedEffect = guard(intEffect) | ||
| Env.run(5): | ||
| Env.get[Int].map: i => | ||
| guardedEffect.map: j => | ||
| Var.run(2): | ||
| Var.get[Int].map: k => | ||
| guardedEffect.map: l => | ||
| i + j + k + l | ||
|
|
||
| val eff = Env.get[Int].map(i => Var.get[Int].map(j => i + j)) | ||
|
|
||
| assert(Env.run(10)(Var.run(20)(genericFn(eff))).eval == 67) | ||
| } | ||
|
|
||
| "cannot be used outside of scope" in { | ||
| def genericFunction[S](using Tag[S]): Guard[S] < S = | ||
| Guard.use[S](g => g) | ||
|
|
||
| assertDoesNotCompile: | ||
| """genericFunction[Any].map(guard => guard(42))""" | ||
| } | ||
|
|
||
| end GuardTest | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is quite useful! would it cover the
Abortcase as well? I think not because of the erased tags, right?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe
Maskfor the name could work better? It seems to express well what the effect does since it masks the effect tags. We havefiber.maskbut it's a rare low-level operation so the name isn't use yetThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't cover the Abort case because it cannot distinguish between different effect types included in an effect intersection. I changed
GuardtoMaskthough.