Skip to content

Guarded effect with Guard API#1403

Open
johnhungerford wants to merge 5 commits intogetkyo:mainfrom
johnhungerford:guard-effect
Open

Guarded effect with Guard API#1403
johnhungerford wants to merge 5 commits intogetkyo:mainfrom
johnhungerford:guard-effect

Conversation

@johnhungerford
Copy link
Collaborator

@johnhungerford johnhungerford commented Aug 1, 2025

Addresses #1387 and #1381

Problem

Writing functions with generic effect types is dangerous when consuming generic effects and composing them effects that need to be handled within the scope of the function. For instance, introducing Scope in composition with some generic effect and then handling the scope will handle any Scope suspensions that may exist in the generic effect. Similarly, handling Abort effects for common public error types like Throwable or Closed will potentially handle those failures in generic effects without intending to, leading to undefined behavior.

Solution

  • Introduce Guarded[S] which protects S from being handled. Guarded[S] is just a suspended A < S that can be trivially unsuspended.
  • Introduce Guard[S] utility which lifts A < S to A < Guarded[S]
  • Guard[S] can only be used within a context provided by Guard.use, which runs Guarded[S] back to S so that Guarded[S] can never escape the scope in which its used. This means you can use it within a generic function without having to worry about accidentally handling a Guarded[X] introduced elsewhere.

Notes

  • A more specific approach is needed for error handling, since it may be necessary to handle generic errors while also isolating them from specific potentially overlapping error types within a scope. Ensure parallel map Closed error propagation #1393 introduces Abort.Mask to solve this problem.
  • I tried providing a more isolated approach that can guard specific effect types within a generic intersection using a version of what @fwbrasil suggested in another PR, but I could only make it work for ArrowEffect which seems too restrictive to me.
  • Guarded[S] is limited by the fact that it has no Isolate which means it can't be used in generic concurrent processes. It would be possible to derive an Isolate if Guarded could isolate an effect for protection from an intersection, which the version in this PR cannot do.

Copy link
Collaborator

@ahoy-jon ahoy-jon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧠 !

@@ -0,0 +1,95 @@
package kyo

class GuardTest extends Test:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add tests with multiple effects? It'd be nice to have scenarios where only part of the effects are guarded

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This approach does not support masking specific effects within a larger effect intersection. I added a test with multiple effects though.

* @see
* [[Guard]]
*/
sealed trait Guarded[S] extends ArrowEffect[[A] =>> A < S, Id]
Copy link
Collaborator

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 Abort case as well? I think not because of the erased tags, right?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe Mask for the name could work better? It seems to express well what the effect does since it masks the effect tags. We have fiber.mask but it's a rare low-level operation so the name isn't use yet

Copy link
Collaborator Author

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 Guard to Mask though.

* @tparam S
* The effect type that can be guarded
*/
sealed class Guard[S](using tag: Tag[Guarded[S]]):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we avoid allocating this class? I think something like this could work:

opaque type Guard[S] = Unit

object Guard:
  extension [S](self: Guard[S])
     def apply[A](effect: A < S)(using CanGuard, Frame, Tag[Guard[S]]): A < Guarded[S]

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

* 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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not only this approach composes better and has ContextEffect support but it's also is cheaper than the version I had suggested! My version would reinterpret all suspensions within the guarded computation, this version just suspends once 👏

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It suspends once because it guards the full effect. While I find it very, very elegant, I still want to see what happen on the cases we would need.

def apply[A, S2](effect: A < (S & S2))(using CanGuard, Frame): A < (Guarded[S] & S2)

(which your version was probably doing)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is the big limitation of my approach. One implication is that you can't fork Guarded effects because it's not possible to construct an Isolate for it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe with something that "just" filter/rewrite tags when needed?(that would be a change in the kernel to pass an actual Mask/Guard on tags)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The big problem is that ContextEffect suspensions are simply KyoDefer instances without any tags identifying the effect type, so it's not possible to identify and mask them. To support a more general masking effect, we would need to allow including tags on KyoDefer, or add a new Kyo subtype specifically for context effects.

@johnhungerford johnhungerford requested a review from fwbrasil August 4, 2025 13:32
@johnhungerford
Copy link
Collaborator Author

Changed Guard/Guarded to Mask/Masked

@ahoy-jon
Copy link
Collaborator

ahoy-jon commented Sep 7, 2025

If I understood correctly, the next kernel will support that! Should we close it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants