Norbi
03/07/2023, 8:29 PMraise()
.
The code in my next message references different catch()
functions, and their return type and semantic is different as well.
How should I refactor the second eagerEffect {}
call to make it execute fold()
in the background?object Err1
object Err2
context(Raise<Err1>)
fun f() {
val r1: Unit = eagerEffect {
f1()
}.catch { // 1
raise(Err1)
}
val r2: Raise<Err2>.() -> Unit = eagerEffect {
f2()
}.catch { // 2
raise(Err1)
}
}
context(Raise<Err1>)
fun f1() {}
context(Raise<Err2>)
fun f2() {}
The first catch()
is a member of `Raise<R>`:
arrow.core.raise.Raise.catch(kotlin.jvm.functions.Function1<? super arrow.core.raise.Raise<? super R>,? extends A>, kotlin.jvm.functions.Function2<? super arrow.core.raise.Raise<? super R>,? super java.lang.Throwable,? extends A>)
The second catch()
is a top-level function:
arrow.core.raise.RaiseKt.catch(kotlin.jvm.functions.Function1<? super arrow.core.raise.Raise<? super E>,? extends A>, kotlin.jvm.functions.Function2<? super arrow.core.raise.Raise<? super E>,? super java.lang.Throwable,? extends A>)
getOrElse()
function, it seems to do what I expected:
inline fun <E, A> EagerEffect<E, A>.getOrElse(onRaise: (E) -> A): A = fold( { onRaise(it) }, ::identity)
val r2: Unit = eagerEffect {
f2()
}.getOrElse {
raise(Err1)
}
simon.vergauwen
03/07/2023, 8:46 PMf2
inside Raise<Err1>
would be, and I would actually suggest the same for the f1
.
context(Raise<Err1>)
fun f() {
catch({ f1() }) { raise(Err1) }
recover(
{ f2() },
{ _: Err2 -> raise(Err1) }
) { _: Throwable -> raise(Err1) }
}
eagerEffect
or effect
unless you want to explicitly return (suspend) Raise<E>.() -> A
.
f2()
is not callable within Raise<Err1>
, since the error don't match. So the only way to do it is use recover
to transform Err2
to Err1
and then you can still also provide an exception handler.getOrElse
you only wanted to transform E
? Then you can rewrite to:
context(Raise<Err1>)
fun f() {
f1()
recover({ f2() }) { _: Err2 -> raise(Err1) }
}
Norbi
03/07/2023, 9:30 PMThere should never be a need to wrap inBut on the top level, where nooreagerEffect
effect
Raise<R>
is available yet, I should use eagerEffect {}
and effect {}
, shouldn't I?simon.vergauwen
03/07/2023, 9:46 PMalpha
you're on but on the latest 1.1.6-alpha.44
you should be able to just call following code on the top-level:
fun main() {
recover({ f() }) { e: Err1 -> println(e) }
}
fold
directly:
fun main() {
fold(
{ f() },
{ t: Throwable -> },
{ e: Err1 -> },
{ res: Unit -> }
)
// which is implementation of:
eagerEffect { f() }.fold(
{ t: Throwable -> },
{ e: Err1 -> },
{ res: Unit -> }
)
}
recover
you'll see it's also fold
, and if you check the impl of Effect.getOrElse
you'll see it's recover
. So in the end everything is fold
😅Norbi
03/07/2023, 10:42 PMfeedback
For me, code like
recover(
{ f2() },
{ _: Err2 -> raise(Err1) }
) { _: Throwable -> raise(Err1) }
is not very readable.
Maybe it's just me but I would prefer a syntax closer to Kotlin's try-catch, like
attempt { f2() }
.handle { _: Err2 -> raise(Err1) }
// and
attempt { f2() }
.handle(
catch = { _: Throwable -> raise(Err1) },
recover = { _: Err2 -> raise(Err1) }
)
Something like the following? (Quick and dirty prototype, only with recover
without catch
, etc.)
context(r@Raise<R>)
@RaiseDSL
inline fun <R, E, A> (Raise<E>.() -> A).handle(
@BuilderInference recover: Raise<R>.(E) -> A
): A {
val action = this
return fold<E, A, A>({ action(this) }, { throw it }, { recover(this@r, it) }, { it })
}
fun <R, A> attempt(@BuilderInference block: Raise<R>.() -> A) = block
EDIT: as I read it again, I have reinvented the wheel, this is very similar to the syntax of effect {}.fold(...)
! 🙂simon.vergauwen
03/07/2023, 11:14 PMeffect { } recover { }
, effect { }.recover({ }, { })
or effect { } catch { }
. (same for eagerEffect
).effect
or eagerEffect
, same with attempt
here it would need a suspend
alternative as well.
recover({ f() }) { e: Err1 -> }
doesn't, neither does catch({ f() }) { e: Throwable -> }
.
recover
with 3 lambdas which you mentioned above reads especially bad. 😞 Nesting of course also works, in either order.
catch({
recover({ f() }) { e: Err1 -> }
}) { t: Throwable -> }
recover({
catch({ f() }) { t: Throwable -> }
}) { e: Err1 -> }
Norbi
03/08/2023, 8:33 AM"problem" is that you need to useIt's not a problem for me, its usage results in very readable code imho. What confused me a bit was:oreffect
eagerEffect
that's the syntax you already get fromvs... works in both DSL, and top-level formeffect { } recover { }
There should never be a need to wrap inBut if I can choose betweenoreagerEffect
effect
recover({}) {}
and effect {} recover {}
I choose the latter for sure 🙂
This leads us back to my original question, where those "conflicting" catch()
functions are a bit confusing for someone new to the "raise() mechanism".
Thanks for the explanations!simon.vergauwen
03/08/2023, 8:50 AMcatch
on Err1
inside Raise<Err1>
. The second one didn't since it's of type Err2
.
But if I can choose betweenThere is nothing preventing you from doing so,andrecover({}) {}
I choose the latter for sure 🙂effect {} recover {}
recover
is the base operation. Calling effect { f() } recover { }
is literally calling recover({ effect { f() }.bind() }) { }
.
What I meant with:
There should never be a need to wrap inBeside preferringoreagerEffect
effect
effect { } recover { }
which I'll put aside as personal syntax preference, there is never a need to return Raise<E>.() -> A
or suspend Raise<E>.() -> A
as a return value when using context receivers.
I am personally more a much bigger fan of recover({ f() }) { }
because it's more consistent, having many different syntaxes available is confusing. So taking into account your feedback I would be in favor of removing the `recover`/`catch` DSL functions for `Effect`/`EagerEffect` and that changes your originally snippet to:
context(Raise<Err1>)
fun f() {
val r1: Unit = eagerEffect {
f1()
}.catch { // 1
raise(Err1)
}.bind()
val r2: Raise<Err2>.() -> Unit = eagerEffect {
f2()
}.catch { // 2
raise(Err1)
}//.bind() not available
}
What do you think about that? It's indeed more consistent, but I thought having `catch`/`recover` auto-bind and colouring it with DSL syntax in IDEA would be sufficiently clear.catch
is pink and the other one shows up as a top-level extension color. The latter also required an additional import. Thinking about it though, maybe autobind is a bit confusing indeed 🤔 Thank you for the feedback @Norbi 🙌 WDYT with this extra context? Would you be in favor of always requiring explicit bind
?Norbi
03/08/2023, 10:51 AMbind()
call is not needed (especially because it won't help in my biggest problem, that in my example f1()
is effectively invoked while f2()
is not, in the latter case a fold()
call would be needed as well)
• now I see why `recover()`/`catch()` is more consistent
I'll be back :)simon.vergauwen
03/08/2023, 1:56 PMNorbi
03/08/2023, 3:40 PMcatch {}
calls being optional)?simon.vergauwen
03/08/2023, 3:46 PMf1
you can call bind()
to avoid having to do manually re-raise the Err1
error using getOrElse { raise(it) }
.
For f2
you need to transform Err2
to Err1
by unwrapping using getOrElse
and then you have option to provide a fallback value or raise Err1
.
I added e: Throwable
to catch
to show its working over the exception channel not the error one.
context(Raise<Err1>)
fun f() {
val r1: Unit = eagerEffect {
f1()
}.catch { e: Throwable ->
raise(Err1)
}.bind()
val r2: Unit eagerEffect {
f2()
}.catch { e: Throwable ->
raise(Err1)
}.getOrElse { e: Err2 -> raise(Err1) }
}
catch
is meant to interact with Throwable
from the lazy computation, and recover/getOrElse
is meant to interact with E
from Raise<E>
.val x: Effect<String, Int> = effect { 1 }
val y: Effect<List<Char>, Int> =
x.recover { msg -> raise(msg.toList() }
val z: Int = x.getOrElse { -1 }
val x2: Effect<String, Int> = effect {
throw RuntimeException("Boom!")
}
val y2: Effect<Sting, Int> =
x2.catch { t: Throwable ->
raise(t.message ?: "no-message")
}
val z2: Int =
x2.getOrElse { -1 } // Exception: BOOM!
val z3: Int =
y2.getOrElse { -1 } // -1
Norbi
03/08/2023, 4:42 PMsimon.vergauwen
03/08/2023, 4:43 PMNorbi
03/08/2023, 5:07 PMOnI didn't mean to "reraise" but to simulate thatyou can callf1
to avoid having to do manually re-raise thebind()
errorErr1
Err1
can be raised at that location as well.Any feedback is valuable
...
This POC removes autobind DSL syntax, and favoursI looked at the git diff of your POC, and for me, the explicitas a replacement in most cases.getOrElse
getOrElse {}
calls are fairly well readable (better and more unified than the previous version with recover {}
and fold {}
calls).
I also think that (although I liked it first 🙂 ) removing the autobind syntax improves the consistency of the API.
EDIT: I will refactor my own code to use the new syntax if you decide to merge it, so I will try it soon 🙂simon.vergauwen
03/08/2023, 5:20 PMimproves the consistency of the APIYes, after our discussion I feel the same and I think consistency is key for easier understanding and better readability but lets let it sink in for a couple of days and await some more feedback (hopefully 🤞).
phldavies
03/08/2023, 5:30 PMEffect
, getOrElse
to me hides the fact it’s lazy. Possibly a better name might be invokeOrElse
to align with invoke()
which conversely requires the Raise<E>
context receiver.Effect
and Either
shouldn’t be so interchangeable at the callsite, but the other part disagrees 🙂simon.vergauwen
03/08/2023, 5:34 PMsuspend fun
and the other one is not 😅 The name is indeed not very explicit in that regard, but for me the rationale is that if you'd translate it to boxed types / transformers it'd look like: fun IO<Either<E, A>>.getOrElse(f: IO[A]): IO[A]
. So it's still lazy, pure and safe.Norbi
03/08/2023, 5:36 PMAny feedback from anyone else is also appreciated 🙏Just a suggestion: maybe you should ask for feedback in a separate thread, because this thread has become bloated because of my questions - sorry 🙂
phldavies
03/08/2023, 5:39 PMEither
that already contains a value (or error) that you’re get
’ing (or defaulting), and an Effect
which can be called multiple times potentially with different results (success or failure) on each call. I’ve usually find that’s the confusing aspect of IO
is that it can at times seem to be a value (and indeed could be a pure value) or it could be an effect. I like that it’s clear based on signature/type that Effect and Either differ in that, and it should ideally be clear when consuming them that they’re differentEffect<E, A>
to be effectively IO[Either[E, A]]
then the equivalent of Effect.getOrElse
would be fun IO[Either[E, A]].getOrElse(f: IO[A]): A
which implies that getOrElse is invoking/running the effect/IO rather than mapping the boxed type as you’ve indicated. I’d expect if that was the intent the signature would instead be fun Effect<E, A>.getOrElse(f: () -> A): () -> A
simon.vergauwen
03/08/2023, 6:19 PMIO
-> suspend
and Either<E, A>
to Raise<E>.() -> A
you get suspend fun Effect<E, A>.getOrElse(f: suspend (E) -> A): A
. No?phldavies
03/08/2023, 6:22 PMEffect
itself being the IO
and didn’t think getOrElse
was suspend
one isI thought you were talking aboutand the other one is notsuspend fun
Effect
being suspend
and Either
is not 😉 but you meant that Effect.getOrElse
is suspend
, and Either.getOrElse
is not 🙂simon.vergauwen
03/08/2023, 6:28 PMsuspend fun
, or another Effect
. Effectively only resolving the Raise<E>
constraint.Francis Reynders
03/09/2023, 8:00 AMinvokeWithCatch
? That would reflect exactly what it does and avoid name clashing with the Effect
simon.vergauwen
03/10/2023, 10:04 AMgetOrElse
makes more sense in this case? I feel it's the same as toEither
given the constraint of suspend
.
@Francis Reynders thanks for the feedback. I try to follow a general rule which is if the alias name is longer or similar in length as the composition of names of the smaller parts if often doesn't really offer a true benefit. In this case I think it doesn't make sense. I.e.
effect.invokeWithCatch {
// ...
}
effect.catch {
// ...
}.bind()
The latter is/seems shorter in API name, and I think this would also result in a more consistent API. If we consider that introducing new names increases API surface complexity. WDYT?Francis Reynders
03/10/2023, 11:38 AM.catch
.bind
combination seemed less accessible for newer users (such as myself), but understanding bind
should probably be considered a basic understanding of the lib.Norbi
03/10/2023, 2:18 PMIn this case however theI also don't like the explicit.catch
combination seemed less accessible for newer users (such as myself).bind
bind()
... It may be very logical for someone with a strong FP background but for me "bind" means almost nothing in this context (I bind what to what? 🙂 ).simon.vergauwen
03/10/2023, 2:30 PMbind
can be always be avoided when working with context receivers, but then you need to fully embrace DSL style.
object Err1
object Err2
context(Raise<Err1>) fun f1(): Int = 1
context(Raise<Err2>) fun f2(): Int = 2
context(Raise<Err1>)
fun example() : Int {
val w = f1()
val x = catch({ f1() }) { _: Throwable -> raise(Err1) }
val y = recover(
{ f2() },
{ _: Err2 -> raise(Err1) },
{ _: Throwable -> raise(Err1) }
)
val z = recover({ f2() }) { e: Err2 -> raise(Err1) }
return w + z
}
Given the original example ignored all the results (Unit
) everywhere then the compiler cannot help you catch any missing bind
since Unit
accepts Unit
but also Effect<E, A>
values. If you would add : Int
in the return types like I've done here then the code wouldn't have compiled.It may be very logical for someone with a strong FP background but for me "bind" means almost nothing in this context (I bind what to what? 🙂 ).We're trying to optimise for beginners, but autobind also seems beginner unfriendly as you mentioned in your original message that started the discussion 😅 You can always also use
invoke()
rather than bind
but I feel it looks a bit weird.
context(Raise<Err1>)
fun example() {
// effect { f1() }.catch { raise(Err1) }.invoke()
effect { f1() }.catch { raise(Err1) }()
}
Norbi
03/10/2023, 5:47 PMbut I feel it looks a bit weirdYou wrote earlier that you prefer using
recover({}) {}
the most.
As I experiment with the API I found that besides effect {}.recover {}
I like recover()
as well, what I don't like is its name 🙂
Wouldn't it be a better name execute()
or perform()
or invoke()
or something else?
I started using it like this, and although it is a little bit verbose, it is very well readable imho:
recover(
action = { ... },
recover = { ... },
catch = { ... }
)
It reflects the "two layers" of error handling very well...simon.vergauwen
03/10/2023, 5:51 PMfold(action, catch, recover, transform)
.
So would make most sense to have an fold
operation with ::identity
as a default parameter for transform
but not sure that won't conflict with other signatures 🤔
The reason there is a recover
operation with a catch
overload is to avoid having to write:
catch({
recover(action, recover)
}, catch)