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
}
}simon.vergauwen
07/16/2022, 1:08 PMEffect.attempt here would return Effect<Throwable, A>simon.vergauwen
07/16/2022, 1:09 PMEffect 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
}simon.vergauwen
07/25/2022, 12:41 PMeffect<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
}simon.vergauwen
07/26/2022, 9:06 AMeffect 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 lambdasimon.vergauwen
07/26/2022, 9:07 AMYannick Lazzari
07/26/2022, 12:54 PM