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

orDie combinator #902

Merged
merged 15 commits into from
Dec 7, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Added mapAbort
  • Loading branch information
johnhungerford committed Dec 5, 2024
commit e23f858547c7f1231b37c3b7485f02e3dd618209
19 changes: 2 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3416,7 +3416,7 @@ val anyAbort: Int < Abort[ErrorType] = 1
val unsafeAgain: Int < Any = anyAbort.orPanic
```

The Abort-specific error handling methods are as follows:
Other error-handling methods are as follows:

```scala
import kyo.*
Expand All @@ -3428,6 +3428,7 @@ trait C
val effect: Int < Abort[A | B | C] = 1

val handled: Result[A | B | C, Int] < Any = effect.result
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 }

Expand All @@ -3439,22 +3440,6 @@ val aToEmpty: Int < Abort[Absent | B | C] = effect.forAbort[A].toEmpty
val aToChoice: Int < (Choice & Abort[B | C]) = effect.forAbort[A].toChoice
```

Note that `mapError` and similar ZIO methods are not really needed, as we can handle those cases with `catching` (and `forAbort[E].catching`):

```scala
import kyo.*

trait A
trait B

val effect: Int < Abort[A] = 1

def fn(err: A): B = new B {}
val mapped: Int < Abort[B] = effect.catching(err => Kyo.fail(fn(err)))

def impureFn(err: A): B < Async = new B {}
val mappedImpure: Int < (Abort[B] & Async) = effect.catching(err => impureFn(err).map(Kyo.fail))
```

## Acknowledgements

Expand Down
34 changes: 34 additions & 0 deletions kyo-combinators/shared/src/main/scala/kyo/Combinators.scala
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,22 @@ extension [A, S, E](effect: A < (Abort[E] & S))
): Result[E, A] < S =
Abort.run[E](effect)

/** Handles the Abort effect, transforming caught errors into a new error as determined by mapping function
*
* @return
* A computation that fails with Abort[E1], where E1 is an error type mapped from E
*/
def mapAbort[E1, S1](
fn: E => E1 < S1
)(
using
ct: SafeClassTag[E],
ct1: SafeClassTag[E1],
fl: Flat[A],
fr: Frame
): A < (Abort[E1] & S & S1) =
effect.catching(e => fn(e).map(Kyo.fail))

def forAbort[E1 <: E]: ForAbortOps[A, S, E, E1] = ForAbortOps(effect)

/** Translates the Abort effect to a Choice effect.
Expand Down Expand Up @@ -451,6 +467,24 @@ 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 a partial Abort[E1] effect, transforming caught errors into a new error as determined by mapping function
*
* @return
* A computation that fails with Abort[E2], where E2 is an error type mapped from E1
*/
def mapAbort[ER, E2, S1](
fn: E1 => E2 < S1
)(
using
ev: E => E1 | ER,
ct: SafeClassTag[E1],
ct1: SafeClassTag[E2],
reduce: Reducible[Abort[ER]],
fl: Flat[A],
fr: Frame
): A < (Abort[E2] & reduce.SReduced & S & S1) =
catching(e => fn(e).map(Kyo.fail))

/** Handles the partial Abort[E1] effect and applies a recovery function to the error.
*
* @return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class AbortCombinatorTest extends Test:
}
}

"handle" - {
"result" - {
"should handle" in {
val effect1 = Abort.fail[String]("failure")
assert(effect1.result.eval == Result.fail("failure"))
Expand All @@ -84,14 +84,6 @@ class AbortCombinatorTest extends Test:
assert(handled.eval == Result.success(Result.success(Result.success(Result.fail("failure")))))
}

"should handle abort" in {
val effect: Int < Abort[String] =
Abort.fail[String]("failure")
val handled: Result[String, Int] < Any =
effect.result
assert(handled.eval == Result.fail("failure"))
}

"should handle union" in {
val failure: Int < Abort[String | Boolean | Double | Int] =
Abort.fail("failure")
Expand All @@ -101,6 +93,22 @@ class AbortCombinatorTest extends Test:
}
}

"mapAbort" - {
"should map abort" in {
val effect1 = Abort.fail[String]("failure")
val effect1Mapped = effect1.mapAbort(_.size)
assert(effect1Mapped.result.eval == Result.fail(7))
}

"should map union abort" in {
val effect1 = Abort.fail[String | Int | Boolean]("failure")
val effect1Mapped = effect1.mapAbort:
case str: String => 0
case _ => -1
assert(effect1Mapped.result.eval == Result.fail(0))
}
}

"convert" - {

"should convert all abort to empty" in {
Expand Down Expand Up @@ -244,6 +252,18 @@ class AbortCombinatorTest extends Test:
}
}

"mapAbort" - {
"should map single Abort" in {
val effect1 = Abort.fail[String | Int | Boolean]("failure")
val effect1Mapped = effect1.forAbort[String].mapAbort(_.size)
assert(effect1Mapped.result.eval == Result.fail(7))

val effect2 = Abort.fail[String | Int | Boolean](1)
val effect2Mapped = effect2.forAbort[String].mapAbort(_.size)
assert(effect2Mapped.result.eval == Result.fail(1))
}
}

"toChoice" - {
"should convert some abort to choice" in {
val effect: Int < Abort[String | Boolean] = Abort.fail("error")
Expand Down
Loading