Skip to content
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

Error handle without panic #1042

Merged
merged 36 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
22f7e6f
Added Result.Partial type alias, Abort.runPartial, Abort.fold, and co…
johnhungerford Jan 21, 2025
3f071b1
Fixed tests
johnhungerford Jan 21, 2025
647a2b0
Merged main
johnhungerford Jan 21, 2025
60a89df
Added note about runPartial and fold to README
johnhungerford Jan 21, 2025
e769aea
Updated readme
johnhungerford Jan 21, 2025
50b6e3f
Using opaque type for Result.Partial
johnhungerford Jan 21, 2025
a96d948
Removed Partial#toResult since its a subtype
johnhungerford Jan 21, 2025
9073dd5
Improved CanEqual givens
johnhungerford Jan 21, 2025
1611e65
Reverted CanEqual nonsense
johnhungerford Jan 21, 2025
3e88f30
Added Result.Partial tests
johnhungerford Jan 22, 2025
5799aaf
Fixed README
johnhungerford Jan 22, 2025
893a314
Using runWith for run, runPartial, and runFold
johnhungerford Jan 28, 2025
3b2a1e6
Update Result fold API to match abort
johnhungerford Jan 28, 2025
35dc485
Merge remote-tracking branch 'upstream/main' into error-handle-withou…
johnhungerford Jan 28, 2025
fc1cda6
Updated Result
johnhungerford Jan 28, 2025
f030dcb
minor fix
johnhungerford Jan 28, 2025
d2cbd12
Starting to fix tests
johnhungerford Jan 28, 2025
0bc9d42
runwith private
johnhungerford Jan 28, 2025
8e489e2
Updated abort combinators
johnhungerford Jan 28, 2025
7c595a9
Fixed combinators tests
johnhungerford Jan 28, 2025
b13da54
Fixed some tests
johnhungerford Jan 29, 2025
0371e98
Updated Result scaladoc
johnhungerford Jan 29, 2025
8ea2c37
Fixed cats test
johnhungerford Jan 29, 2025
fe68cf1
Fixed retry test
johnhungerford Jan 29, 2025
f9f1cbc
Reverted test changes
johnhungerford Jan 29, 2025
24ec280
Reverted more tests
johnhungerford Jan 29, 2025
1baab4d
Reverted and cleaned up
johnhungerford Jan 30, 2025
ee0e788
Fixed sttp JS
johnhungerford Jan 30, 2025
65ba948
Merge branch 'main' into error-handle-without-panic
fwbrasil Jan 31, 2025
370328e
Merge remote-tracking branch 'upstream/main' into error-handle-withou…
johnhungerford Feb 1, 2025
224f397
Added ...OrThrow methods to Abort combinators
johnhungerford Feb 1, 2025
fdc8641
Merge branch 'error-handle-without-panic' of github.com:johnhungerfor…
johnhungerford Feb 1, 2025
6b569a2
Updated README
johnhungerford Feb 1, 2025
356c18a
Fixed lingering foldError
johnhungerford Feb 1, 2025
ce0121b
Made RunWithOps private
johnhungerford Feb 1, 2025
787cd7c
Minor change to docstring
johnhungerford Feb 3, 2025
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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,8 @@ println(t"A: ${aRes.eval}, B: ${bRes.eval}")
// Output: A: Success(1), B: Fail(failed!)
```

Note that `Kyo` has two error channels: an explicitly typed channel represented by `Abort[E]` as well as a `Throwable` "panic" channel for unexpected errors. The `Result` generated by `Abort.run` includes both `Failure[E]` and `Panic` error cases. To handle `Failure[E]` without also handling `Panic`, you can use `Abort.runPartial`, which will produce a `Result.Partial[E, A]`. `Result.Partial[E, A]` is just a type alias for `Success[A] | Failure[E]`, and will allow you to handle success and failure cases without having to handle panics. Alternatively, you can use `Abort.fold`, which is overloaded to handle either all three cases or success and failure.

> Note that the `Abort` effect has a type parameter and its methods can only be accessed if the type parameter is provided.

### IO: Side Effects
Expand Down Expand Up @@ -3530,13 +3532,19 @@ trait C
val effect: Int < Abort[A | B | C] = 1

val handled: Result[A | B | C, Int] < Any = effect.result
val handledWithoutPanic: Result.Partial[A | B | C, Int] < Any = effect.partialResult
val folded: String < Any = effect.foldAbort(_.toString, _.toString, _.toString)
val foldedWithoutPanic: String < Any = effect.foldAbort(_.toString, _.toString)
val mappedError: Int < Abort[String] = effect.mapAbort(_.toString)
val caught: Int < Any = effect.catching(_.toString.size)
val partiallyCaught: Int < Abort[A | B | C] = effect.catchingSome { case err if err.toString.size > 5 => 0 }
val swapped: (A | B | C) < Abort[Int] = effect.swapAbort

// Manipulate single types from within the union
// Handle error types within the union
val handledA: Result[A, Int] < Abort[B | C] = effect.forAbort[A].result
val handledWithoutPanicA: Result.Partial[A, Int] < Abort[B | C] = effect.forAbort[A].partialResult
val foldedA: String < Abort[B | C] = effect.forAbort[A].fold(_.toString, _.toString, _.toString)
val foldedWithoutPanicA: String < Abort[B | C] = effect.forAbort[A].fold(_.toString, _.toString)
val caughtA: Int < Abort[B | C] = effect.forAbort[A].catching(_.toString.size)
val partiallyCaughtA: Int < Abort[A | B | C] = effect.forAbort[A].catchingSome { case err if err.toString.size > 5 => 0 }
val aSwapped: A < Abort[Int | B | C] = effect.forAbort[A].swap
Expand Down
142 changes: 142 additions & 0 deletions kyo-combinators/shared/src/main/scala/kyo/Combinators.scala
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,19 @@ extension [A, S, E](effect: A < (Abort[E] & S))
): Result[E, A] < S =
Abort.run[E](effect)

/** Handles the Abort effect and returns its result as a `Result.Partial[E, A]`, not handling Panic exceptions.
*
* @return
* A computation that produces a partial result of this computation with the Abort[E] effect handled
*/
def partialResult(
using
ct: SafeClassTag[E],
fl: Flat[A],
fr: Frame
): Result.Partial[E, A] < S =
Abort.runPartial[E](effect)

/** Handles the Abort effect, transforming caught errors into a new error as determined by mapping function
*
* @return
Expand Down Expand Up @@ -370,6 +383,61 @@ extension [A, S, E](effect: A < (Abort[E] & S))
case Result.Success(v) => v
}

/** Recovers from an Abort failure by applying the provided function.
*
* This method allows you to handle failures in an Abort effect and potentially continue the computation with a new value. It only
* handles failures of type E and leaves panics unhandled (Abort[Nothing]).
*
* @param onSuccess
* A function that takes the success value of type A and returns a new computation
* @param onFail
* A function that takes the failure value of type E and returns a new computation
* @param v
* The original computation that may fail
* @return
* A computation that either succeeds with the original value or the recovered value
*/
def foldAbort[B, S1](
onSuccess: A => B < S1,
onFail: E => B < S1
)(
using
ct: SafeClassTag[E],
fl1: Flat[A],
fl2: Flat[B],
fr: Frame
): B < (S & S1) =
Abort.fold[E](onSuccess, onFail)(effect)

/** Recovers from an Abort failure by applying the provided function.
*
* This method allows you to handle failures in an Abort effect and potentially continue the computation with a new value. It only
* handles failures of type E and leaves panics unhandled (Abort[Nothing]).
*
* @param onSuccess
* A function that takes the success value of type A and returns a new computation
* @param onFail
* A function that takes the failure value of type E and returns a new computation
* @param onPanic
* A function that takes the throwable panic value and returns a new computation
* @param v
* The original computation that may fail
* @return
* A computation that either succeeds with the original value or the recovered value
*/
def foldAbort[B, S1](
onSuccess: A => B < S1,
onFail: E => B < S1,
onPanic: Throwable => B < S1
)(
using
ct: SafeClassTag[E],
fl1: Flat[A],
fl2: Flat[B],
fr: Frame
): B < (S & S1) =
Abort.fold[E](onSuccess, onFail, onPanic)(effect)

/** Handles the Abort effect and applies a partial recovery function to the error.
*
* @return
Expand Down Expand Up @@ -492,6 +560,21 @@ class ForAbortOps[A, S, E, E1 <: E](effect: A < (Abort[E] & S)) extends AnyVal:
): Result[E1, A] < (S & reduce.SReduced) =
Abort.run[E1](effect.asInstanceOf[A < (Abort[E1 | ER] & S)])

/** Handles the partial Abort[E1] effect and returns its result as a `Result.Partial[E1, A]`.
*
* @return
* A computation that produces the result of this computation with the Abort[E1] effect handled
*/
def partialResult[ER](
using
ev: E => E1 | ER,
ct: SafeClassTag[E1],
reduce: Reducible[Abort[ER]],
fl: Flat[A],
frame: Frame
): Result.Partial[E1, A] < (S & reduce.SReduced) =
Abort.runPartial[E1](effect.asInstanceOf[A < (Abort[E1 | ER] & S)])

/** Handles a partial Abort[E1] effect, transforming caught errors into a new error as determined by mapping function
*
* @return
Expand Down Expand Up @@ -531,6 +614,65 @@ class ForAbortOps[A, S, E, E1 <: E](effect: A < (Abort[E] & S)) extends AnyVal:
case ab @ Result.Panic(_) => Abort.get(ab.asInstanceOf[Result[Nothing, Nothing]])
})

/** Recovers from an Abort failure by applying the provided function.
*
* This method allows you to handle failures in an Abort effect and potentially continue the computation with a new value. It only
* handles failures of type E and leaves panics unhandled (Abort[Nothing]).
*
* @param onSuccess
* A function that takes the success value of type A and returns a new computation
* @param onFail
* A function that takes the failure value of type E and returns a new computation
* @param v
* The original computation that may fail
* @return
* A computation that either succeeds with the original value or the recovered value
*/
def fold[B, S1, ER](
onSuccess: A => B < S1,
onFail: E1 => B < S1
)(
using
ct: SafeClassTag[E1],
ev: E => E1 | ER,
reduce: Reducible[Abort[ER]],
fl1: Flat[A],
fl2: Flat[B],
fr: Frame
): B < (S & S1 & reduce.SReduced) =
Abort.fold[E1](onSuccess, onFail)(effect.asInstanceOf[A < (Abort[E1 | ER] & S)])

/** Recovers from an Abort failure by applying the provided function.
*
* This method allows you to handle failures in an Abort effect and potentially continue the computation with a new value. It only
* handles failures of type E and leaves panics unhandled (Abort[Nothing]).
*
* @param onSuccess
* A function that takes the success value of type A and returns a new computation
* @param onFail
* A function that takes the failure value of type E and returns a new computation
* @param onPanic
* A function that takes the throwable panic value and returns a new computation
* @param v
* The original computation that may fail
* @return
* A computation that either succeeds with the original value or the recovered value
*/
def fold[B, S1, ER](
onSuccess: A => B < S1,
onFail: E1 => B < S1,
onPanic: Throwable => B < S1
)(
using
ct: SafeClassTag[E1],
ev: E => E1 | ER,
reduce: Reducible[Abort[ER]],
fl1: Flat[A],
fl2: Flat[B],
fr: Frame
): B < (S & S1 & reduce.SReduced) =
Abort.fold[E1](onSuccess, onFail, onPanic)(effect.asInstanceOf[A < (Abort[E1 | ER] & S)])

/** Handles the partial Abort[E1] effect and applies a partial recovery function to the error.
*
* @return
Expand Down
122 changes: 119 additions & 3 deletions kyo-combinators/shared/src/test/scala/kyo/AbortCombinatorTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ class AbortCombinatorTest extends Test:
}
}

"catch" - {
"catching" - {
"should catch all abort" in {
val failure: Int < Abort[String] =
Abort.fail("failure")
Expand Down Expand Up @@ -253,6 +253,64 @@ class AbortCombinatorTest extends Test:
}
}

"foldAbort" - {
"should handle success and fail case, throwing panics, when two handlers provided" in {
val success: Int < Abort[String] = 23
val handledSuccess: String < Any =
success.foldAbort(
i => i.toString,
identity
)
assert(handledSuccess.eval == "23")
val failure: Int < Abort[String] =
Abort.fail("failure")
val handledFailure: String < Any =
failure.foldAbort(
i => i.toString,
identity
)
assert(handledFailure.eval == "failure")
val panic: Int < Abort[String] = Abort.panic(Exception("message"))
try
panic.foldAbort(
i => i.toString,
identity
).eval
succeed
catch
case e: Exception => assert(e.getMessage == "message")
end try
}

"should handle all cases when three handlers provided" in {
val success: Int < Abort[String] = 23
val handledSuccess: String < Any =
success.foldAbort(
i => i.toString,
identity,
_.getMessage
)
assert(handledSuccess.eval == "23")
val failure: Int < Abort[String] =
Abort.fail("failure")
val handledFailure: String < Any =
failure.foldAbort(
i => i.toString,
identity,
_.getMessage
)
assert(handledFailure.eval == "failure")
val panic: Int < Abort[String] = Abort.panic(Exception("message"))
val handledPanic: String < Any =
panic.foldAbort(
i => i.toString,
identity,
_.getMessage
)
assert(handledPanic.eval == "message")
}
}

"swap" - {
"should swap abort" in {
val failure: Int < Abort[String] = Abort.fail("failure")
Expand Down Expand Up @@ -344,7 +402,7 @@ class AbortCombinatorTest extends Test:
}
}

"caught" - {
"catching" - {
"should catch some abort" in {
val effect: Int < Abort[String | Boolean] = Abort.fail("error")
val caught = effect.forAbort[String].catching(_ => 99)
Expand All @@ -360,7 +418,7 @@ class AbortCombinatorTest extends Test:
}
}

"caughtPartial" - {
"catchingSome" - {
"should catch some abort with partial function" in {
val effect: Int < Abort[String | Boolean] = Abort.fail("error")
val caught = effect.forAbort[String].catchingSome {
Expand Down Expand Up @@ -411,6 +469,64 @@ class AbortCombinatorTest extends Test:
val handled3: Result[String | Int, Int] < Abort[Boolean] = effect3.forAbort[String | Int].result
assert(Abort.run[Any](handled3).eval == Result.fail(true))
}

"fold" - {
"should handle success and fail case, throwing panics, when two handlers provided" in {
val success: Int < Abort[String | Boolean] = 23
val handledSuccess: String < Abort[Boolean] =
success.forAbort[String].fold(
i => i.toString,
identity
)
assert(Abort.run[Boolean](handledSuccess).eval == Result.Success("23"))
val failure: Int < Abort[String | Boolean] =
Abort.fail("failure")
val handledFailure: String < Abort[Boolean] =
failure.forAbort[String].fold(
i => i.toString,
identity
)
assert(Abort.run[Boolean](handledFailure).eval == Result.Success("failure"))
val panic: Int < Abort[String | Boolean] = Abort.panic(Exception("message"))
try
panic.forAbort[String].fold(
i => i.toString,
identity
)
succeed
catch
case e: Exception => assert(e.getMessage == "message")
end try
}

"should handle all cases when three handlers provided" in {
val success: Int < Abort[String] = 23
val handledSuccess: String < Any =
success.foldAbort(
i => i.toString,
identity,
_.getMessage
)
assert(handledSuccess.eval == "23")
val failure: Int < Abort[String] =
Abort.fail("failure")
val handledFailure: String < Any =
failure.foldAbort(
i => i.toString,
identity,
_.getMessage
)
assert(handledFailure.eval == "failure")
val panic: Int < Abort[String] = Abort.panic(Exception("message"))
val handledPanic: String < Any =
panic.foldAbort(
i => i.toString,
identity,
_.getMessage
)
assert(handledPanic.eval == "message")
}
}
}

"orPanic" - {
Expand Down
8 changes: 7 additions & 1 deletion kyo-data/shared/src/main/scala/kyo/Result.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,16 @@ import scala.util.control.NonFatal
*/
opaque type Result[+E, +A] >: (Success[A] | Error[E]) = Success[A] | Error[E]

object Result:
private trait LowPriorityImplicits:
given flatSuccess[A](using Flat[A]): Flat[Success[A]] =
Flat.unsafe.bypass

object Result extends LowPriorityImplicits:

import internal.*

type Partial[+E, +A] = Success[A] | Failure[E]
johnhungerford marked this conversation as resolved.
Show resolved Hide resolved

/** Creates a Result from an expression that might throw an exception.
*
* @param expr
Expand Down
Loading
Loading