This might be more related to context receivers bu...
# arrow
d
This might be more related to context receivers but I've encountered the problem when working with Raise DSL so hopefully it's ok to ask it here. The example code doesn't need any dependencies except arrow-core. The following code doesn't compile (kotlin 2.0.0), complaining that
withAccount
is missing the
Raise<Any>
context.
Copy code
fun <R, E : Any> withApiTraceError(
    block: context(Account, Raise<E>) () -> R,
    exceptionProducer: (E) -> Exception,
): R {
    return withApiError(exceptionProducer) {
        withAccount {
            block(this@withAccount, this@withApiError)
        }
    }
}

private fun <R, E : Any> withApiError(
    exceptionProducer: (E) -> Exception = { IllegalStateException("") },
    block: Raise<E>.() -> R,
): R = recover(block) { error -> throw exceptionProducer(error) }

context(Raise<Any>)
fun <T> withAccount(block: Account.() -> T): T = TODO()

data object Account
Yet, if I use the default
exceptionProducer
in
withApiError
like this
Copy code
fun <R, E : Any> withApiTraceError(
    block: context(Account, Raise<E>) () -> R,
    exceptionProducer: (E) -> Exception,
): R {
    return withApiError { // using default here
        withAccount {
            block(this@withAccount, this@withApiError)
        }
    }
}
then it compiles fine. What's going on?
y
Your
E
in
withApiTraceError
might not be Any, and so withApiTraceError's
exceptionProducer
might not accept
Any
, hence you have a
Raise<E>
in context instead of a
Raise<Any>
The default version works because now
withApiError
has no constraints from parameters on its
E
, except that its lambda calls
withAccount
which expects a
Raise<Any>
receiver (more accurately context), and so
E
is inferred to be
Any
(this is called builder inference) Are you sure what you want is a
Raise<Any>
on
withAccount
?
d
I probably understand now. Previously I had a similar working code, only without the
exceptionProducer
params (the exception was hardcoded). That worked even with
context(Raise<Any>)
on
withAccount
because the
E
in
withApiError
was inferred via builder inference (as you have pointed out). The solution is simply to write
Copy code
context(Raise<E>)
fun <E : Any, T> withAccount(block: Account.() -> T): T = TODO()
To complete my understanding: If
recover
params were not annotated with
@BuilderInference
then even my original version (without any
exceptionProducer
s) wouldn't compile, probably complaining about "type cannot be inferred", right?
y
@BuilderInference
is enabled by default since 1.8? so the annotation does nothing FYI. But before that was implemented yes it would likely complain that there's no info to infer the type (although maybe it could've guessed it as Any simply because that's the most-permissive
Raise
) Your new
withAccount
can't actually do anything with the
Raise
it's given (because
E
is unknown, and hence nothing can be raised). Are you sure you need a
Raise
context at all? If you're doing this to force the user to have some
Raise
, you should probably use
Raise<*>
instead
d
Of course, the reality is a little bit more complex but the main principle is the same.
withAccount
calls another function that needs a Raise context. And that function finally `raise`s something. So I have
Copy code
context(Raise<E>)
fun <E : Any, T> withAccount(block: Account.() -> T): T = with(getAccount(), block)

context(Raise<NotFound>)
fun getAccount() = findAccount() ?: raise(NotFound)
Does it make sense or is there still a room for improvement?
Oh, I've realized now that even this doesn't work (problem in withAccount)...
To be more clear about what I want to achieve: I have an
ApiError
base class and I want to indicate that the whole
withApiError
machinery works with some subclass of
ApiError
. How should I define the contexts of the functions above?
To be even more precise, I want to indicate that
withApiError
can within its block handle any subclasses of
ApiError
(
NotFound
is one such subclass).
y
Well logically
withAccount
needs
Raise<NotFound>
. It doesn't need anything with generics because
Raise
is declared
in
.
withApiError
can stay exactly the same. Now, in
withAPITraceError
the issue arises that you need a
Raise<NotFound>
and a
Raise<E>
. The best way to do that is probably something like:
Copy code
fun <R, E : Any> withApiTraceError(
    block: context(Account, Raise<E>) () -> R,
    exceptionProducer: (E) -> Exception,
    onNotFound: () -> Exception
): R {
    return withApiError(exceptionProducer) {
        recover({
        withAccount {
            block(this@withAccount, this@withApiError)
        }
        }) { _: NotFound -> throw onNotFound() }
    }
}
If you want that
withApiError
must handle all subclasses of
ApiError
then just concretize it:
Copy code
private fun <R> withApiError(
    exceptionProducer: (ApiError) -> Exception = { IllegalStateException("") },
    block: Raise<ApiError>.() -> R,
): R = recover(block) { error -> throw exceptionProducer(error) }
d
Yes, I've realized exactly that right now 🙂 I.e. there's no need in
exceptionProducer
to take the concrete
E
subclass but simply any ApiError subclass. Thanks!