Skip to content

Commit

Permalink
[core] introduce Async.apply and clarify the distinction between IO a…
Browse files Browse the repository at this point in the history
…nd Async (#986)
  • Loading branch information
fwbrasil authored Jan 9, 2025
1 parent 588a782 commit 4ca493b
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 10 deletions.
41 changes: 38 additions & 3 deletions kyo-core/shared/src/main/scala/kyo/Async.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,18 @@ import scala.concurrent.Future
import scala.util.NotGiven
import scala.util.control.NonFatal

/** Represents an asynchronous computation effect.
/** Asynchronous computation effect.
*
* This effect provides methods for running asynchronous computations, creating and managing fibers, handling promises, and performing
* parallel and race operations.
* While IO handles pure effect suspension, Async provides the complete toolkit for concurrent programming - managing fibers, scheduling,
* and execution control. It includes IO in its effect set, making it a unified solution for both synchronous and asynchronous operations.
*
* This separation, enabled by Kyo's algebraic effect system, is reflected in the codebase's design: the presence of Async in pending
* effects signals that a computation may park or involve fiber scheduling, contrasting with IO-only operations that run to completion.
*
* Most application code can work exclusively with Async, with the IO/Async distinction becoming relevant primarily in library code or
* performance-critical sections where precise control over execution characteristics is needed.
*
* This effect includes IO in its effect set to handle both async and sync execution in a single effect.
*
* @see
* [[Async.run]] for running asynchronous computations
Expand All @@ -31,6 +39,33 @@ object Async:

sealed trait Join extends ArrowEffect[IOPromise[?, *], Result[Nothing, *]]

/** Convenience method for suspending computations in an Async effect.
*
* While IO is specifically designed to suspend side effects without handling asynchronicity, Async provides both side effect
* suspension and asynchronous execution capabilities (fibers, async scheduling). Since Async includes IO in its effect set, this
* method allows users to work with a single unified effect that handles both concerns.
*
* Note that this method only suspends the computation - it does not fork execution into a new fiber. For concurrent execution, use
* Async.run or combinators like Async.parallel instead.
*
* This is particularly useful in application code where the distinction between pure side effects and asynchronous execution is less
* important than having a simple, consistent way to handle effects. The underlying effects are typically managed together at the
* application boundary through KyoApp.
*
* @param v
* The computation to suspend
* @param frame
* Implicit frame for the computation
* @tparam A
* The result type of the computation
* @tparam S
* Additional effects in the computation
* @return
* The suspended computation wrapped in an Async effect
*/
inline def apply[A, S](inline v: => A < S)(using inline frame: Frame): A < (Async & S) =
IO(v)

/** Runs an asynchronous computation and returns a Fiber.
*
* @param v
Expand Down
20 changes: 13 additions & 7 deletions kyo-core/shared/src/main/scala/kyo/IO.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,21 @@ import kyo.Tag
import kyo.kernel.*
import kyo.kernel.internal.Safepoint

/** Represents an IO effect for handling side effects in a pure functional manner.
/** Pure suspension of side effects.
*
* IO allows you to encapsulate and manage side-effecting operations (such as file I/O, network calls, or mutable state modifications)
* within a purely functional context. This enables better reasoning about effects and helps maintain referential transparency.
* Unlike traditional monadic IO types that combine effect suspension and async execution, Kyo leverages algebraic effects to cleanly
* separate these concerns. IO focuses solely on suspending side effects, while async execution (fibers, scheduling) is handled by the
* Async effect.
*
* Like Async includes IO, this effect includes Abort[Nothing] to represent potential panics (untracked, unexpected exceptions). IO is
* implemented as a type-level marker rather than a full ArrowEffect for performance. Since Effect.defer is only evaluated by the Pending
* type's "eval" method, which can only handle computations without pending effects, side effects are properly deferred. This ensures they
* can only be executed after an IO.run call, even though it is a purely type-level operation.
* This separation enables an important design principle in Kyo's codebase: methods that only declare IO in their pending effects are run
* to completion without parking or locking. This property, combined with Kyo's lock-free primitives, makes it easier to reason about
* performance characteristics and identify potential async operations in the code.
*
* IO is implemented as a type-level marker rather than a full ArrowEffect for performance. Since Effect.defer is only evaluated by the
* Pending type's "eval" method, which can only handle computations without pending effects, side effects are properly deferred. This
* ensures they can only be executed after an IO.run call, even though it is a purely type-level operation.
*
* Like Async includes IO, this effect includes Abort[Nothing] to represent potential panics (untracked, unexpected exceptions).
*/
opaque type IO <: Abort[Nothing] = Abort[Nothing]

Expand Down
38 changes: 38 additions & 0 deletions kyo-core/shared/src/test/scala/kyo/AsyncTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1176,4 +1176,42 @@ class AsyncTest extends Test:
}
}

"apply" - {
"suspends computation" in run {
var counter = 0
val computation = Async {
counter += 1
counter
}
for
v1 <- computation
v2 <- computation
v3 <- computation
yield
assert(v1 == 1)
assert(v2 == 2)
assert(v3 == 3)
assert(counter == 3)
end for
}

"preserves effects" in run {
var executed = false
for
started <- Latch.init(1)
done <- Latch.init(1)
fiber <- Async.run {
started.release.andThen {
Async { executed = true }.andThen {
done.release
}
}
}
_ <- started.await
_ <- done.await
yield assert(executed)
end for
}
}

end AsyncTest

0 comments on commit 4ca493b

Please sign in to comment.