Yannick Lazzari
07/15/2022, 3:54 PMeffect { ... }
block. Given the following function:
fun function1: Effect<ErrorAdt, String> = effect {
val val1: String = function2()
val val2: String = function3()
return val1 + val2
}
and that both function2
and function3
are possibly throwing exceptions, what is the best way to convert any exceptions they might throw into my ErrorAdt
? Should I simply wrap each call to the functions inside try/catch
blocks and convert them there? Or is there another error handling function that I can call on the effect, where I can centralize all error mapping logic, using some pattern matching on any thrown exceptions? I can't seem to find any in the docs, but just curious. Thank you!stojan
07/15/2022, 4:25 PMsimon.vergauwen
07/16/2022, 1:07 PMeffect
because this is quite a common use-case when wrapping foreign APIs.
https://github.com/arrow-kt/arrow/pull/2746
@Alejandro Serrano Mena proposed a new API recently which is closely related to this, but only works over typed errors R
. I've been thinking how we can nicely add another API to complement this but for Throwable
.
effect<ErrorAdt, String> {
Effect.attempt {
function2()
}.refineOrThrow<IllegalStateException>()
.catch { illegalState ->
// recover with value,
// or shift ErrorAdt
}
}
Effect.attempt
here would return Effect<Throwable, A>
Effect
a regular class, which then can extend suspend EffectScope<R>.() -> A
so we would automatically get the catch
syntax as designed by @Alejandro Serrano Mena in the PR.Yannick Lazzari
07/18/2022, 12:54 PMeffect<ErrorAdt, String> {
suspendingFunctionThatCanThrow1();
suspendingFunctionThatCanThrow2();
}.handleThrowable { t ->
// recover with value or shift to ErrorAdt
}
If either the first or second function throws an exception, the handleThrowble
method is invoked, otherwise if the body of the effect block results in a value or the error ADT, that is returned instead. So more alongs the lines of what handleError
and handleErrorWith
do, but for throwables. Would that make sense? It looks a bit more succinct like that I find.than_
07/18/2022, 2:23 PMcontext(EffectScope<ERR>)
@OptIn(ExperimentalTypeInference::class)
fun <ERR, A> attempt(@BuilderInference f: suspend () -> A): suspend EffectScope<ERR>.() -> A = { f() }
context(EffectScope<ERR>)
suspend inline infix fun <ERR, A> (suspend EffectScope<ERR>.() -> A).handleThrowable(
crossinline recover: suspend (Throwable) -> A
): A =
effect(this@handleThrowable).fold(
error = { recover(it.nonFatalOrThrow()) },
recover = { shift(it) },
transform = ::identity
)
seems to be working really nice:
context(EffectScope<ApplicationError>)
suspend fun getChannel(radioCode: String): ChannelRes =
attempt {
service.getChannel(radioCode = radioCode)
} handleThrowable {
shift(...) // boring error domain mapping
}
simon.vergauwen
07/25/2022, 12:40 PMResult<A>
from Kotlin Std.
Then you can achieve following syntax as well.
effect<ErrorAdt, String>{
val x = runCatching {
suspendingFunctionThatCanThrow1()
}.bind { t -> ErrorAdt.First(t) }
val y = runCatching {
suspendingFunctionThatCanThrow2()
}.bind { t -> ErrorAdt.Second(t) }
x + y
}
effect<ErrorAdt, A> {
// no exceptions will be caught here
}.handleThrowable { t ->
// throwable can still be resolved safely
}
Yannick Lazzari
07/25/2022, 1:21 PMhandleThrowable
function above, which I had also suggested earlier in the thread? In terms of API, it looks consistent with the other handleError
and handleErrorWith
functions that already exist for Effect
. And definitely less verbose than the other suggestion above. There may be good reasons you'd still want to surround each function call with its own error handling code, because handling errors for each function is so different that it's best to separate it out like that, but I can also definitely see a lot of cases where you really don't care and simply want to shift on some error value for any throwable that's thrown, in which case having this handleThrowable
definitely could be handy. I mean, people can easily write their own extension function that does something similar, but it would be useful to have something out of the box. Just a thought!simon.vergauwen
07/25/2022, 1:23 PMfun <E, A> Effect<E, A>.handleThrowable(
recover: suspend EffectScope<E>.(Throwable) -> A
): Effect<E, A> = effect {
fold(
{ t -> recover(this@effect, t) },
{ e -> shift(e) },
{ a -> a }
)
}
Yannick Lazzari
07/25/2022, 1:25 PMsimon.vergauwen
07/25/2022, 1:25 PMeffect<E, A> {
attempt {
effectA()
} catch { e: E ->
A
}.catchThrowable { t : Throwable ->
A
}
}.catch { e: E ->
}.catchThrowable { t : Throwable ->
}
Yannick Lazzari
07/25/2022, 1:31 PMeffectA()
returns an Effect
already, and you have a function for handling errors on the Effect
type directly, wouldn't this be more like:
effect<E, A> {
effectA().catch { e: E ->
A
}.catchThrowable { t : Throwable ->
A
}
}.catch { e: E ->
}.catchThrowable { t : Throwable ->
}
It doesn't seem like it would be necessary to wrap the call to effectA()
with attempt
, and even wonder what would be the point of attempt
if you have all the error handling capabilities on the Effect
type directly. When would you choose to still wrap an effect call with attempt
as oppose to use the catchXXX
or handleXXX
functions?simon.vergauwen
07/26/2022, 9:05 AMattempt
is that it allows you to run Effect<E, A>
within effect<E2, B>
. For example,
fun effectA(): Effect<Int, Boolean> = effect { shift(-1) }
effect<String, Int> {
val x = attempt { effectA().bind() } catch { i: Int -> i }
x + 1
}
effect
here has error type String
we can run an effect with error type Int
within attempt
as long as we resolve the error in catch
or shift
an error of type String
within the catch
lambdaYannick Lazzari
07/26/2022, 12:54 PM