I've just run into an interesting problem related ...
# arrow
n
I've just run into an interesting problem related to
raise()
. 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?
Copy code
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>`:
Copy code
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:
Copy code
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>)
If I use my
getOrElse()
function, it seems to do what I expected:
Copy code
inline fun <E, A> EagerEffect<E, A>.getOrElse(onRaise: (E) -> A): A = fold( { onRaise(it) }, ::identity)
Copy code
val r2: Unit = eagerEffect {
            f2()
        }.getOrElse {
            raise(Err1)
        }
s
I'm not sure if that is a problem, the API was designed like this intentionally 😅 Very open to feedback of course! 🙏 There is always top-level, and DSL based behavior where the API and signature is the same. The "correct" way of call
f2
inside
Raise<Err1>
would be, and I would actually suggest the same for the
f1
.
Copy code
context(Raise<Err1>)
fun f() {
  catch({ f1() }) { raise(Err1) }
  recover(
   { f2() },
   { _: Err2 -> raise(Err1) }
  ) { _: Throwable -> raise(Err1) }
}
There should never be a need to wrap in
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.
If you're using
getOrElse
you only wanted to transform
E
? Then you can rewrite to:
Copy code
context(Raise<Err1>)
fun f() {
  f1()
  recover({ f2() }) { _: Err2 -> raise(Err1) }
}
n
There should never be a need to wrap in
eagerEffect
or
effect
But on the top level, where no
Raise<R>
is available yet, I should use
eagerEffect {}
and
effect {}
, shouldn't I?
s
Not sure which
alpha
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:
Copy code
fun main() {
  recover({ f() }) { e: Err1 -> println(e) }
}
You can also call
fold
directly:
Copy code
fun main() {
  fold(
    { f() },
    { t: Throwable -> },
    { e: Err1 -> },
    { res: Unit -> }
  )
  // which is implementation of:
  eagerEffect { f() }.fold(
    { t: Throwable -> },
    { e: Err1 -> },
    { res: Unit -> }
  )
}
If you check the impl of
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
😅
n
feedback
For me, code like
Copy code
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
Copy code
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.)
Copy code
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(...)
! 🙂
s
Yes, that's the syntax you already get from
effect { } recover { }
,
effect { }.recover({ }, { })
or
effect { } catch { }
. (same for
eagerEffect
).
That syntax works in both DSL, and top-level form.
"problem" is that you need to use
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.
Copy code
catch({
  recover({ f() }) { e: Err1 -> }
}) { t: Throwable -> }

recover({
  catch({ f() }) { t: Throwable -> }
}) { e: Err1 -> }
n
"problem" is that you need to use
effect
or
eagerEffect
It's not a problem for me, its usage results in very readable code imho. What confused me a bit was:
that's the syntax you already get from
effect { } recover { }
... works in both DSL, and top-level form
vs
There should never be a need to wrap in
eagerEffect
or
effect
But if I can choose between
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!
s
In the IDEA the first one should've coloured in the DSL syntax style, while the latter didn't. That should've visually indicated that two different methods were being called. The first one works in DSL style since your'e calling
catch
on
Err1
inside
Raise<Err1>
. The second one didn't since it's of type
Err2
.
But if I can choose between
recover({}) {}
and
effect {} recover {}
I choose the latter for sure 🙂
There is nothing preventing you from doing so,
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 in
eagerEffect
or
effect
Beside preferring
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:
Copy code
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.
What I meant with color is that the first
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
?
n
I have to think about it, now that I understand the whole thing better :) But I think • "autobind" seems to be a good feature, it is good that an explicit
bind()
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 :)
s
Small POC making some changes to visual it better, https://github.com/arrow-kt/arrow/pull/2968
n
Do I understand correctly that my example would look like the following (with the
catch {}
calls being optional)?
s
Yes, and no 😅 On
f1
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.
Copy code
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) }
}
Seems there is some confusion.
catch
is meant to interact with
Throwable
from the lazy computation, and
recover/getOrElse
is meant to interact with
E
from
Raise<E>
.
Copy code
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
n
I think I still have to learn to be able to help you in the decision. So let's use the syntax you find better 😉🙂
On
f1
you can call
bind()
to avoid having to do manually re-raise the
Err1
error
I didn't mean to "reraise" but to simulate that
Err1
can be raised at that location as well.
Any feedback is valuable
...
This POC removes autobind DSL syntax, and favours
getOrElse
as a replacement in most cases.
I looked at the git diff of your POC, and for me, the explicit
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 🙂
s
improves the consistency of the API
Yes, 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 🤞).
p
(sorry not fully followed the thread) but a small comment would be that for
Effect
,
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.
part of me feels that
Effect
and
Either
shouldn’t be so interchangeable at the callsite, but the other part disagrees 🙂
s
Well, one is
suspend 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.
n
Any 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 🙂
p
Agreed - my main thought though is to be explicit in the difference between an
Either
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 different
If we consider
Effect<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
s
When turning
IO
->
suspend
and
Either<E, A>
to
Raise<E>.() -> A
you get
suspend fun Effect<E, A>.getOrElse(f: suspend (E) -> A): A
. No?
p
Ah I see what you mean. I was thinking in terms of
Effect
itself being the
IO
and didn’t think
getOrElse
was
suspend
one is
suspend fun
and the other one is not
I thought you were talking about
Effect
being
suspend
and
Either
is not 😉 but you meant that
Effect.getOrElse
is
suspend
, and
Either.getOrElse
is not 🙂
s
Yes, indeed. Sorry if I was unclear 😅 So you're forced to call it from another
suspend fun
, or another
Effect
. Effectively only resolving the
Raise<E>
constraint.
f
I personally like the autobind. At the same time I realize it may be confusing that the actual function changes depending on enclosing context and it's type. A compromise may be to rename one of the functions? e.g. the autobind function could be named
invokeWithCatch
? That would reflect exactly what it does and avoid name clashing with the
Effect
s
Thanks for the feedback @phldavies. Does
getOrElse
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.
Copy code
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?
f
@simon.vergauwen: Understood and good point. In this case however the
.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.
n
In this case however the
.catch
.bind
combination seemed less accessible for newer users (such as myself)
I also don't like the explicit
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? 🙂 ).
s
Well, it really depends on what you're doing.
bind
can be always be avoided when working with context receivers, but then you need to fully embrace DSL style.
Copy code
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.
Copy code
context(Raise<Err1>)
fun example() {
  // effect { f1() }.catch { raise(Err1) }.invoke()
  effect { f1() }.catch { raise(Err1) }()
}
n
but I feel it looks a bit weird
You 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:
Copy code
recover(
    action = { ... },
    recover = { ... },
    catch = { ... }
)
It reflects the "two layers" of error handling very well...
s
The most base operation, from where all of these APIs are derived is
fold(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:
Copy code
catch({
  recover(action, recover)
}, catch)