https://kotlinlang.org logo
#arrow
Title
# arrow
y

Youssef Shoaib [MOD]

03/24/2024, 11:37 AM
Playing around with Compose and Molecule a bit, and I'm realising quickly that
Raise
doesn't mesh nicely with it because Compose doesn't fully support exceptions. Anyone have some experience with this and knows if there's a way to improve the ergonomics of Compose + Raise?
c

CLOVIS

03/24/2024, 11:59 AM
Hey! Composable functions should be purely data → UI mappers, there shouldn't be no business logic directly in them. Thus, there should be no need to
Raise
in a
@Composable
functions themselves. However, that's different in event handlers, since those are allowed to have business logic, suspend, etc. What I do is create a simple function that introduces a
Raise
scope and reports it to a
MutableState
;
Copy code
@Composable
fun SomeForm(…) {
    var failure by remember { mutableStateOf<DomainFailure?>(null) }

    …your form…

    SubmitButton(
        onClick = {
            recover(
                block = { …business logic… },
                recover = { failure = it } 
            )
        }
    )
}
I also have a few helpers for this style of errors in Pedestal (my state management library compatible with Arrow):
Copy code
@Composable
fun ShowUser(userId: String) {
    val user by remember { userService.get(userId).collectAsState() }

    user.onFailure {
        Text("Could not get user")
    }

    user.onSuccess {
        ShowUser(it)
    }

    user.onLoading {
        LoadingSpinner()
    }
}
y

Youssef Shoaib [MOD]

03/24/2024, 12:29 PM
So what I wanted to do is something akin to your last example:
Copy code
@Composable
fun Raise<UserError>.ShowUser(userId: String) {
    val user by remember { userService.get(userId).collectAsState() }

    if (user is Loading) LoadingSpinner() else ShowUser(user.bind())
}
(Can't seem to find an
Outcome.bind()
from a quick skim thru Pedestal, but just imagine this is an
Either
for the sake of the argument) So that someone on the outside can provide the
Text("Could not get user")
part. I'm guessing that likely isn't supported
c

CLOVIS

03/24/2024, 1:22 PM
Outcome.bind()
is here, but yeah
Outcome
is the same thing as
Either
. It exists as a parallel to
ProgressiveOutcome
, which adds the in-progress variants. When context receivers arrive, they will all finally be fully interchangeable through
Raise
.
So that someone on the outside can provide the
Text("Could not get user")
part. I'm guessing that likely isn't supported
Sorry, I don't understand this.
y

Youssef Shoaib [MOD]

03/24/2024, 1:23 PM
I.e. someone can do:
Copy code
recover({ ShowUser("foo") }) { Text("Could not get user") }
c

CLOVIS

03/24/2024, 1:25 PM
recover
is inline, right? So you can do that 🤔
y

Youssef Shoaib [MOD]

03/24/2024, 1:26 PM
Yes, but Compose throws an error about a start/end imbalance.
c

CLOVIS

03/24/2024, 1:27 PM
but
recover({ ShowUser("foo") }…)
makes no sense:
ShowUser
is a data→UI mapper, it shouldn't contain logic, so there is no legitimate case where it raises. It's not just an ideological decision: composable functions recompose, so "calling a composable function" means something completely different than calling a regular function. Because of caching, it may not actually be called, or it may be called multiple times.
y

Youssef Shoaib [MOD]

03/24/2024, 1:29 PM
Hmmmm, I see what you mean, I guess there's no guarantee we can "go outside" to the parent composable. One curious thing though is that you can do something similar with a return type. E.g. if we returned an
Either<Unit, UserError>
, then it'd work just fine. Btw, for my use case, I'm using Compose to produce data, not UI. I'm trying to play around with Molecule to make List Comprehensions, and not being able to
Raise
has made handling certain corner cases harder.
c

CLOVIS

03/24/2024, 1:30 PM
One curious thing though is that you can do something similar with a return type. E.g. if we returned an
Either<Unit, UserError>
, then it'd work just fine.
Yes, because it binds the return value to the parent composable, so if it changes the parent also recomposes.
y

Youssef Shoaib [MOD]

03/24/2024, 1:31 PM
Yes, so it there was a way to do that but with exceptions as well, then
Raise
would work very well.
c

CLOVIS

03/24/2024, 1:31 PM
Btw, for my use case, I'm using Compose to produce data, not UI. I'm trying to play around with Molecule to make List Comprehensions, and not being able to
Raise
has made handling certain corner cases harder.
Yeah, in that case I think you're stuck with returning
Either
. Compose really is made to produce a tree, not returning data directly…
depending on what you're doing, you could produce a tree in which each node is a
Raise
-ble function, and then folding the tree later
blob thinking upside down 1
s

simon.vergauwen

03/25/2024, 8:24 AM
So what happens on other exceptions? I am assuming the limitation is you cannot
try/catch
outside of
@Composable
but only in a single node of the tree?
c

CLOVIS

03/25/2024, 8:32 AM
@Composable
functions are often called by the runtime itself rather than by their parent, so thrown exceptions are in the runtime's stack and you can never catch them. https://www.reddit.com/r/androiddev/comments/19evp8n/how_do_you_handle_uncaught_exceptions_in_compose/
s

simon.vergauwen

03/25/2024, 9:47 AM
Right.. Since a leaf can be redrawn, and thus not bubble up perse.. Feels really strange though. Seems that
Compose
is truly a sublanguage, it's not just Kotlin.
c

CLOVIS

03/25/2024, 9:48 AM
Yeah. You kinda have to think each function call as adding a task to a queue.
But yeah, composable functions should be pure, and that includes not throwing exceptions.
Essentially, it's a different mental model using the same syntax as Kotlin, instead of some custom templating language
2 Views