So now the question will be: how do we get `Raise`...
# arrow-contributors
c
So now the question will be: how do we get
Raise
-like implicit short-circuiting in a world of Rich Errors?
s
I imagined something like this but a better API 😅 A kind-of integration layer
Copy code
context(raise: Raise<E>)
fun <E : Error, A> (E | A).getOrRaise(): A = when(this) {
   is E -> raise(this)
   else -> this
}
c
At minimum I think we need an
ensure
equivalent
s
Copy code
context(raise: Raise<E>)
fun <E : Error, A> ensureNotError(value: E | A): A = when(value) {
   is E -> raise(value)
   else -> value
}
+ contracts
c
Right, I mean a method that I can call within
Copy code
fun foo(a: Int): String | Negative {
    ensure(a >= 0) { Negative }
}
s
Ah, I guess that would need some DSL no?
Copy code
fun foo(a: Int): String | Negative = error {
    ensure(a >= 0) { Negative }
}

fun <E : Error, A> error(block: Raise<E>.() -> A): E | A = recover(block) { it }
c
I think you could create a
Copy code
interface Propagate<E : Error>
special interface to have
Copy code
context(propagate: Propagate<E>)
fun ensure(predicate: Boolean, ...): Unit
and have a special rule that the compiler auto-inserts
.ifError { return it }
on them @Youssef Shoaib [MOD] @Alejandro Serrano.Mena what do you think? I think it could be useful as a first-party immediately actually
This kind of short-circuiting is an important part of error handling in Kotlin and I really dislike that rich errors aren't compatible with it
I'm thinking that if we could opt-in to implicit propagation on the declaration-site, then all the Raise-like primitives can be rebuilt on top of that
> Ah, I guess that would need some DSL no? > With the KEEP as currently written, yeah, and indeed it's easy to write but it requires an ugly
.ifError { return it }
on every call-site to convert back
y
Thanks for the ping! I envision something exactly like Simon's
error { }
block there, and
getOrRaise
, but probably named
bind()
TL;DR: IMO propagation based on return type alone doesn't give you enough control.
Raise
is the perfect abstraction here, so I'm happy rich errors aren't trying to reinvent this wheel. Rich errors work nicely alongside Raise, but neither can replace the other. A compiler plugin can help here with minimal magic. IIUC, your idea with
Propagate
is to have it auto-inserted as a context in error-returning functions, and have it as a context parameter for functions that should auto-propagate. I don't like implicit propagation based on return type because it gives you less control; with
Raise
, I can use builders, or even pass
Raise
instances around and use them with
context
. Also, this
Propagate
context would actually be unsafe without extra checks because it's
return
based, not exception-based, so it needs the same checks as non-local returns.
Raise
can go beyond because it uses exceptions. You're on the right track though. Lifetimes KEEP has non-local returns crossing such boundaries as an integral part, so in a lifetimes world, Raise could just be implemented on top of them. Anyway, it's a lot simpler to insert a
Raise
context based on return type (thus giving you control still). I'm very confident that that's possible as a compiler plugin. The way I see it:
fun foo(): Foo | SomeError
is for errors that should be treated as values and dealt with almost immediately.
first()
,
toInt()
, etc are examples of that. Their errors will probably be very specific and are ones you're unlikely to want propagated (e.g.
NoSuchElement
,
ParsingError
respectively).
context(_: Raise<SomeError>) fun foo(): Foo
is for errors that should be propagated by default. They're domain-specific, so they'll be propagated through your codebase, and get handled in a few places.
.bind()
and
error { }
convert between the worlds (they're called
reflect/reify
pairs in the literature). A compiler plugin that automatically wraps your error-returning functions in
error
can help fill the gaps here, shortening
ifError { return it }
to
.bind()
without too much magic
c
> IIUC, your idea with
Propagate
is to have it auto-inserted as a context in error-returning functions, and have it as a context parameter for functions that should auto-propagate. yup, I agree with the downsides you mention
The way I see it:
fun foo(): Foo | SomeError
is for errors that should be treated as values and dealt with almost immediately.
Right, that's also how I see it, but that's not how the KEEP nor the official communication by JetBrains presents it, so that's not how people will use it.
y
If one doesn't have
Raise
, then they'll make do with rich errors (which is better than the current situation), with
ifError { return it }
appearing a lot (and sometimes
ifError { return@someScope it }
as a replacement for Raise builders). In some sense, propagation is out-of-scope for rich errors; naive propagation would ruin control, so only something like
Raise
is acceptable, which is orthogonal to rich errors themselves (our discussion here is about ~3 new functions, not a complete rehaul!) Re: how the KEEP presents it: it also adds a diagnostic for merging the same error from multiple functions when using
|.
. I believe that was recently softened to a warning, but still. This means that using it for a domain error like
NetworkError
(which fits the Raise criteria I laid above) is not recommended; such a warning shows IMO that you're meant to have many many errors and deal with them pretty quickly (lest you accidentally merge the same error from different functions)
See also this
The examples seem like the "many named errors, deal with them quickly" kind. Example 11 is the exception here, but instead of propagation, they're reusing
handleTransport
when handling is needed.
NetworkFailure
and
UnexpectedFailure
should definitely be `Raise`d instead, and
handleTransport
would work great as a
handleTransport { }
block that handles them. Example 7 might be another exception, but I can't easily see the use site there. Similarly with Example 9. The point remains that most of the examples are not ones you'd want propagated
a
I'm late to the discussion, but the general idea is to use early returns if you need to model short-circuiting
Copy code
fun thing(): Ok | SomeError

fun test(): Else | SomeError {
  thing().ifError { return it }
}
c
Right, but that's my point. There's no way to write something similar to
require
/`check` /`ensure` currently, it's a big downgrade in expressiveness. Hence the thread, I wonder if Arrow could somehow provide that for Rich Errors