Skip to content

Commit

Permalink
[prelude] kernel scaladocs + cleanups (#834)
Browse files Browse the repository at this point in the history
Most of the changes are scaladocs but I also reduced the visibility of
additional APIs and removed a few unnecessary parameters.
  • Loading branch information
fwbrasil authored Nov 10, 2024
1 parent af071d5 commit e739886
Show file tree
Hide file tree
Showing 14 changed files with 973 additions and 155 deletions.
4 changes: 2 additions & 2 deletions kyo-prelude/jvm/src/test/scala/kyo/kernel/BytecodeTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ class BytecodeTest extends Test:

"map" in {
val map = methodBytecodeSize[TestMap]
assert(map == Map("test" -> 18, "anonfun" -> 10, "mapLoop" -> 132))
assert(map == Map("test" -> 18, "anonfun" -> 10, "mapLoop" -> 149))
}

"handle" in {
val map = methodBytecodeSize[TestHandle]
assert(map == Map("test" -> 20, "anonfun" -> 8, "handleLoop" -> 247))
assert(map == Map("test" -> 26, "anonfun" -> 8, "handleLoop" -> 253))
}

def methodBytecodeSize[A](using ct: ClassTag[A]): Map[String, Int] =
Expand Down
324 changes: 270 additions & 54 deletions kyo-prelude/shared/src/main/scala/kyo/kernel/ArrowEffect.scala

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions kyo-prelude/shared/src/main/scala/kyo/kernel/Boundary.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ import scala.annotation.implicitNotFound
import scala.annotation.nowarn
import scala.quoted.*

/** Ensures type-safe transitions of effects across asynchronous boundaries.
*
* When code requests a Boundary (typically through implicit resolution), it means that effects need to be handled before crossing an
* asynchronous boundary. Only [[ContextEffect]]s can cross boundaries, while [[ArrowEffect]]s must be handled first because they represent
* operations that require explicit interpretation.
*
* If the compiler requests a Boundary, either:
* - Handle any non-ContextEffects in the computation before the boundary
* - Or split nested boundary operations into separate statements if you see the "nested Async operations" error
*
* @tparam Ctx
* The context effects that will cross the boundary
* @tparam S
* The source effects that must be handled before crossing
*/
@implicitNotFound("""
Could not create an Async boundary for effects: ${Ctx}
Expand Down
6 changes: 3 additions & 3 deletions kyo-prelude/shared/src/main/scala/kyo/kernel/Context.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import kyo.Flat
import kyo.Tag
import kyo.bug

opaque type Context = Map[Tag[Any] | IsolationFlag, AnyRef]
private[kyo] opaque type Context = Map[Tag[Any] | IsolationFlag, AnyRef]

object Context:
private[kyo] object Context:
inline given Flat[Context] = Flat.unsafe.bypass

val empty: Context = Map.empty
Expand Down Expand Up @@ -43,7 +43,7 @@ object Context:
end set
end extension

private[kyo] object internal:
object internal:
class IsolationFlag
object IsolationFlag extends IsolationFlag
end Context
118 changes: 106 additions & 12 deletions kyo-prelude/shared/src/main/scala/kyo/kernel/ContextEffect.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,98 @@ import kyo.bug
import scala.annotation.nowarn
import scala.util.NotGiven

/** Represents the requirement for a value that will be provided later by a handler.
*
* While ArrowEffect represents functions awaiting implementation, ContextEffect represents values awaiting provision. It captures the need
* for a value of type A without specifying where that value comes from. When a handler provides a value, that value becomes available
* within the handler's scope - once the handler's scope ends, the value is no longer available to computations.
*
* This mechanism aligns with dependency injection patterns - handlers act as injectors that provide values within their scope, and
* different handlers can provide different values in different scopes. The composition of effects automatically tracks these requirements
* through the type system.
*
* Context effects come in two varieties. By default, values are inherited across async boundaries when computations are suspended and
* resumed. Effects that mix the ContextEffect.Isolated trait do not cross async boundaries, requiring fresh values when computation
* resumes asynchronously. This isolation is useful for values that should remain within a single async context, like thread-local data.
*
* The polymorphic type parameter A defines what type of value is required:
* @tparam A
* The type of value that will be provided by a handler
*/
abstract class ContextEffect[+A] extends Effect

object ContextEffect:

/** A marker trait for context effects that do not persist across asynchronous boundaries.
*
* When a context effect extends this trait, its values will not be inherited by child fibers after an asynchronous operation. Instead,
* child fibers start with fresh values, making these effects behave similarly to non-inheritable thread locals.
*/
trait Isolated:
self: ContextEffect[?] =>

inline def suspend[A, E <: ContextEffect[A]](inline tag: Tag[E])(using inline frame: Frame): A < E =
suspendAndMap(tag)(identity)
/** Creates a suspended computation that requests a value from a context effect. This establishes a requirement for a value that must be
* satisfied by a handler higher up in the program. The requirement becomes part of the effect type, ensuring that handlers must
* provide the requested value before the program can execute.
*
* @param effectTag
* Identifies which context effect to request the value from
* @return
* A computation that will receive the requested value when executed
*/
inline def suspend[A, E <: ContextEffect[A]](inline effectTag: Tag[E])(using inline frame: Frame): A < E =
suspendAndMap(effectTag)(identity)

/** Creates a suspended computation that requests a context value and transforms it immediately upon receipt. This combines the
* operations of requesting and transforming a context value into a single step.
*
* @param effectTag
* Identifies which context effect to request the value from
* @param f
* The transformation to apply to the received value
* @return
* A computation containing the transformed value
*/
inline def suspendAndMap[A, E <: ContextEffect[A], B, S](
inline tag: Tag[E]
inline effectTag: Tag[E]
)(
inline f: Safepoint ?=> A => B < S
)(using inline frame: Frame): B < (E & S) =
suspendAndMap(tag, bug("Unexpected pending context effect: " + tag.show))(f)
suspendAndMap(effectTag, bug("Unexpected pending context effect: " + effectTag.show))(f)

/** Requests a value from a context effect with a specified default value. Unlike standard suspend, this version does not create a
* mandatory effect requirement. If no handler provides a value, the computation proceeds with the default value instead. This makes
* the context value optional rather than required.
*
* @param effectTag
* Identifies which context effect to request the value from
* @param default
* The value to use when no handler provides one
* @return
* A computation that provides either the context value or default
*/
inline def suspend[A, E <: ContextEffect[A]](
inline tag: Tag[E],
inline effectTag: Tag[E],
inline default: => A
)(using inline frame: Frame): A < Any =
suspendAndMap(tag, default)(identity)
suspendAndMap(effectTag, default)(identity)

/** Requests an optional context value and transforms it, using a default if no value is available. This combines requesting an optional
* context value with immediate transformation. The transformation function receives either the context value if available or the
* default value if not.
*
* @param effectTag
* Identifies which context effect to request the value from
* @param default
* The value to use when no handler provides one
* @param f
* The transformation to apply to either the context or default value
* @return
* A computation containing the transformed value
*/
@nowarn("msg=anonymous")
inline def suspendAndMap[A, E <: ContextEffect[A], B, S](
inline _tag: Tag[E],
inline effectTag: Tag[E],
inline default: => A
)(
inline f: Safepoint ?=> A => B < S
Expand All @@ -43,21 +109,49 @@ object ContextEffect:
def apply(v: Unit, context: Context)(using Safepoint) =
Safepoint.handle(v)(
suspend = this,
continue = f(context.getOrElse(_tag, default).asInstanceOf[A])
continue = f(context.getOrElse(effectTag, default).asInstanceOf[A])
)

/** Handles a context effect by providing a value for a specific computation scope. This satisfies suspend operations within that scope
* by making the provided value available to them. The handler establishes a region where the context value is defined and can be
* accessed.
*
* @param effectTag
* Identifies which context effect to handle
* @param value
* The value to provide to the computation
* @param v
* The computation requiring the context value
* @return
* The computation result with the context value provided
*/
inline def handle[A, E <: ContextEffect[A], B, S](
inline _tag: Tag[E],
inline effectTag: Tag[E],
inline value: A
)(v: B < (E & S))(
using
inline _frame: Frame,
inline flat: Flat[A]
): B < S =
handle(_tag, value, _ => value)(v)
handle(effectTag, value, _ => value)(v)

/** Handles a context effect by either providing a new value or transforming an existing one. This allows for layered handling of
* context values, where a handler can either establish a new value when none exists or modify a value that was provided by an outer
* handler.
*
* @param effectTag
* Identifies which context effect to handle
* @param ifUndefined
* The value to use when no existing value is found
* @param ifDefined
* The transformation to apply to any existing value
* @param v
* The computation requiring the context value
* @return
* The computation result with the context value handled
*/
inline def handle[A, E <: ContextEffect[A], B, S](
inline _tag: Tag[E],
inline effectTag: Tag[E],
inline ifUndefined: A,
inline ifDefined: A => A
)(v: B < (E & S))(
Expand All @@ -72,7 +166,7 @@ object ContextEffect:
new KyoContinue[IX, OX, EX, Any, B, S](kyo):
def frame = _frame
def apply(v: OX[Any], context: Context)(using Safepoint) =
val tag = _tag // avoid inlining the tag multiple times
val tag = effectTag // avoid inlining the tag multiple times
val updated =
if !context.contains(tag) then context.set(tag, ifUndefined)
else context.set(tag, ifDefined(context.get(tag)))
Expand Down
37 changes: 31 additions & 6 deletions kyo-prelude/shared/src/main/scala/kyo/kernel/Effect.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,35 @@ import kyo.Tag
import scala.annotation.nowarn
import scala.util.control.NonFatal

/** The base trait for all effects in the Kyo effect system.
*
* When code performs an effectful operation, instead of executing immediately, effects create a suspended computation that captures what
* needs to be done. These suspended computations can then be interpreted in different ways through effect handlers.
*
* This suspension mechanism is the foundation of Kyo's effect system. It allows effectful code to be pure and composable - rather than
* performing operations directly, code builds up a description of what operations should occur. This description can then be interpreted
* by handlers that determine how the operations are actually executed.
*
* There are two kinds of effects:
* - [[ArrowEffect]] for suspended computations involving input/output transformations.
* - [[ContextEffect]] for suspended computations requiring contextual values.
*/
abstract class Effect private[kernel] ()

object Effect:

def defer[A, S](f: Safepoint ?=> A < S)(using _frame: Frame): A < S =
new KyoDefer[A, S]:
def frame = _frame
def apply(v: Unit, context: Context)(using Safepoint) =
f

/** Wraps a computation with error handling.
*
* This method allows you to catch and handle exceptions that might occur during the execution of a computation. The error handler `f`
* will be called if a non-fatal exception occurs either during the initial evaluation or during any subsequent effect operations.
*
* @param v
* the effect computation to protect
* @param f
* the error handler function that takes a Throwable and returns a new effect
* @return
* a new effect that will either complete normally or handle exceptions using the provided handler
*/
inline def catching[A, S, B >: A, S2](inline v: => A < S)(
inline f: Throwable => B < S2
)(using inline _frame: Frame, safepoint: Safepoint): B < (S & S2) =
Expand All @@ -42,4 +61,10 @@ object Effect:
f(ex)
end try
end catching

private[kyo] def defer[A, S](f: Safepoint ?=> A < S)(using _frame: Frame): A < S =
new KyoDefer[A, S]:
def frame = _frame
def apply(v: Unit, context: Context)(using Safepoint) =
f
end Effect
Loading

0 comments on commit e739886

Please sign in to comment.