I'm making a library with Compose Runtime (ONLY St...
# compose
c
I'm making a library with Compose Runtime (ONLY State/Snapshot stuff, not UI, not even a mutable tree) and I have a case that looks like:
Copy code
@Composable 
fun foo(): Int {
    return Random.nextInt()
}
Now, my understanding is that this function won't ever rerun, since it has no inputs and reads no state. If so, great, that's what I want. Now, I want two calls of this function in different call stacks to return the same value:
Copy code
@Composable
fun bar() {
    println(foo())

    if (…) {
        println(foo())
    }
}
should both return the same value. How can I achieve something like this?
Maybe this is just about using
derivedStateOf
with regular functions instead of using
@Composable
?
s
https://issuetracker.google.com/issues/206021557 It will in fact be rerun if it happens to be in a recomposition scope which was invalidated so there was a recomposition happening there.
c
And I can't
derivedStateOf
of the result of a
@Composable
function, can I?
s
I'm not sure what you're trying to do here. You can put some mutableState inside a derivedStateOf but a response you get from a composable function isn't a state object automatically. If you put that inside a derivedStateOf as-is it won't do anything really. In fact if you put it around a remember you will even be getting a stale value since derivedStateOf won't automatically know how to listen to its changes.
Maybe if you take a step back and explain what you're trying to do it could help here.
c
I have a system which has “data sources” and “processors”. There are three types of data sources; • immutable sources • mutable sources that can detect their changes (e.g. by subscribing to some kind of event in an external system that will notify them if they change) • mutable sources that cannot detect their changes (basically, all they can do is poll until they change). Data processors are functions that read data sources or other processors, and perform expensive work. I want to avoid running processors when all the data they read hasn't changed, and I want to proactively rerun all the processors that transitively depend on a data source when it changes. The overall system should expose its internal state as atomic state changes, other parts of the system should never be able to observe states that are in the process of being recomputed. A lot of this seems to correspond to Compose concepts; • DisposableEffect is great for modeling external subscriptions • LaunchedEffect is great for modeling work by processors • Snapshot is great for the external atomicity requirement • derivedStateOf seems great to avoid unnecessary work But that's basically where I'm at. The concepts seem to fit, but I'm not sure how to go further than this.
s
Okay I am not gonna lie to you here, so I'll just say that this is way too abstract for me to have any further input, I'm probably not the right person, sorry 😶 For the original snippet however, no you won't be getting the same value back. And you will even get new values seemingly "randomly", which in practice will just be whenever you have a recomposition happening at the call site. If you want the same result back I'm not sure making that function a composable one is the right first step 🤔
c
Thanks for the info 🙏
z
Sounds like you just need to extra the rng into a state holder/view model and pass it around
c
First, non-unit returning functions are never skipped so the first example the function will always be called whenever the caller's scope needs to compose. Changing this is tracked by https://issuetracker.google.com/issues/206021557 which we are considering for use cases like you have. Second, we if do skip it will be positionally memoized, not globally memoized. It will not globally memoize as you asked for. That is, both calls
foo
will return different values. Lastly, you should consider looking at https://github.com/cashapp/molecule which uses Compose for something very much like you describe. The change above, for example, would be a signficant win for Molecule.
x
In the Compose Internals book it says that Composable lambdas are memoized by the compose Compiler:
We can think of a Composable lambda like
@Composable (A, B) -> C
as being equivalently implemented as
State<@Composable (A, B) -> C
. Callsites where the lambda is invoked (
lambda(a, b)
) can then be replaced with the equivalent
lambda.value.invoke(a, b)
I wonder if this would work for what you need:
Copy code
@Composable
fun bar(foo: @Composable () -> Unit) {
    println(foo())

    if (…) {
        println(foo())
    }
}
🤔 1
c
That's very interesting, I'll experiment with it. Is that Compose Internals book available online?
x
I've bought the book from here. It's a great book, I really recommend you taking a look at it. It's also rather dense and it's taking me a while to get progress with it, but it's worth the time spent 🔝
c
This is not correct. Normal lambdas are memoized by the compiler but composable lambdas are not. They are transformed into a
ComposableLambda
instance which tracks when new lambda are created and when the lambda is executed. This allows composition skip the caller of the lambda but ensure the lambda's body is executed. We are considering changing this in the future so that the
ComposableLambda
instance is only modified when the captured values of the lambda change, which is what we do for normal lambdas today.
x
Thanks for the info Chuck! What you're saying here looks accurate and I'm far from an expert in the internals of Compose. So I'm curious: would you say the statement of the book that I cite above is wrong, then?
c
The quote is right.
ComposableLambda
can be though of as a
State<T>
though it is implemented differently. This is not memoization, however.
x
Sound good