Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion kyo-actor/shared/src/main/scala/kyo/Actor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ object Actor:
}.handle(
Sync.ensure(mailbox.close), // Ensure mailbox cleanup by closing it when the actor completes or fails
Env.run(_subject), // Provide the actor's Subject to the environment so it can be accessed via Actor.self
Scope.run, // Close used resources
Scope.run, // Clean up resources
Fiber.init // Start the actor's processing loop in an async context
)
yield new Actor[E, A, B](_subject, _consumer):
Expand Down
61 changes: 61 additions & 0 deletions kyo-prelude/shared/src/main/scala/kyo/Mask.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package kyo

import kyo.Mask.CanMask
import kyo.kernel.ArrowEffect

/** Mask effect(s) [[S]] to protect 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 [[Mask.use]] blocks.
*
* @tparam S
* The effect type that is masked
* @see
* [[Mask]]
*/
sealed trait Masked[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 masked
*/
opaque type Mask[S] = Unit

object Mask:
extension [S](mask: Mask[S])(using tag: Tag[Masked[S]])
/** Mask an effect from being handled, wrapping its effect type [[S]] with [[Masked]]
*
* @param effect
* The effect to be masked
* @return
* A new effect with its effect intersection protected from handling
*/
def apply[A](effect: A < S)(using CanMask, Frame): A < Masked[S] =
ArrowEffect.suspend[A](tag, effect)
end extension

sealed abstract class CanMask
private val canMaskInstance = new CanMask {}

private def run[S](using Frame)[A, S1](effect: A < (Masked[S] & S1))(using tag: Tag[Masked[S]]): A < (S & S1) =
ArrowEffect.handle(tag, effect)(
handle = [C] => (input, cont) => input.map(c => cont(c))
)

/** Use a [[Mask]] 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 [[Masked]][S]. To be used in functions that accept effects with generic effect types in order to avoid
* accidentally handling them. The masked type [[Masked]][S] is handled to [[S]] at the end of the scope.
*
* @tparam S
* Effect type intersection of the effects to be masked within the scope of [[f]]
* @param f
* Function that uses a [[Mask]][S] instance to protect one or more effects from being handled
* @return
* An effect with the original masked effect(s) [[S]] along with any additional effects [[S1]] accumulated when using them
*/
def use[S](using Tag[Masked[S]], Frame)[A, S1](f: CanMask ?=> Mask[S] => A < (Masked[S] & S1)): A < (S & S1) =
given CanMask = canMaskInstance
val mask: Mask[S] = ()
val result = f(mask)
Mask.run[S](result)
end use
end Mask
113 changes: 113 additions & 0 deletions kyo-prelude/shared/src/test/scala/kyo/MaskTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package kyo

class MaskTest 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 =
Mask.use[S]: mask =>
Var.run(2):
Var.get[Int].map: i =>
mask(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)
}

"mask protects context effect" in {
def genericFn[S](intEffect: Int < S)(using Tag[S]): Int < S =
Mask.use[S]: mask =>
Env.run(5):
Env.get[Int].map: i =>
mask(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)
}

"mask protects complex effect" in {
def genericFn[S](intEffect: Int < S)(using Tag[S]): Int < S =
Mask.use[S]: mask =>
val maskedEffect = mask(intEffect)
Env.run(5):
Env.get[Int].map: i =>
maskedEffect.map: j =>
Var.run(2):
Var.get[Int].map: k =>
maskedEffect.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)
}

"combined masked and unmasked effects" in {
def genericFn[S](eff1: Int < S, eff2: Int < S)(using Tag[S]): Int < S =
Mask.use[S]: mask =>
val eff1Masked = mask(eff1)
Env.run(5):
Env.get[Int].map: i => // 5
eff1Masked.map: j => // 10
Var.run(2):
Var.get[Int].map: k => // 2
eff2.map: l => // 2
i + j + k + l

val eff1 = Env.get[Int]
val eff2 = Var.get[Int]

assert(Env.run(10)(Var.run(10)(genericFn(eff1, eff2))).eval == 19)
}

"cannot be used outside of scope" in {
def genericFunction[S](using Tag[S]): Mask[S] < S =
Mask.use[S](g => g)

assertDoesNotCompile:
"""genericFunction[Any].map(mask => mask(42))"""
}

end MaskTest
Loading