I'm interested in writing Arrow Optics integration with Compose's state management, but I'm not how ...
m
I'm interested in writing Arrow Optics integration with Compose's state management, but I'm not how or even if it's possible to create new mutable state types. Creating naïve implementations of
State<T>
and
MutableState<T>
that read and write eagerly seems to work for now (haven't tested snapshotting extensively yet), but I'm concerned that I might need to be observing some types like
dependentStateOf
does. Is there a way to integrate my new state types nicely with the existing state types?
As an example, this is how I implement an
Iso
optic:
Copy code
fun <T, U> MutableState<T>.get(iso: Iso<T, U>): MutableState<U> = IsoMutableState(this, iso)

internal class IsoMutableState<T, U>(
    private val state: MutableState<T>,
    private val iso: Iso<T, U>
) : MutableState<U> {
    override var value: U
        get() = iso.get(state.value)
        set(value) {
            state.value = iso.set(value)
        }

    override fun component1(): U = value

    override fun component2(): (U) -> Unit = { value = it }
}
It seems to work fine, but I don't understand enough about the internals of the Compose state management system to be confident in that assessment.
a
What is it you're trying to accomplish?
2
m
In short, I want to derive a
MutableState<U>
from a
MutableState<T>
and a
Lens<T, U>
with all of the features of
derivedStateOf
.
a
I'm finding this to be a puzzling question because the proposed end goal doesn't seem to be a good fit for design situations encountered when using
mutableStateOf
and friends. Can you give a concrete example of what code patterns you'd like to enable? Given X code that is built using snapshot state, what design or ergonomic problem does adding Lenses solve?
in a sense,
mutableStateOf
is already a lens into a particular value in a snapshot and this seems like an attempt to add another layer of the same, but with an expectation that
MutableState<T>
would become part of a public API somewhere in order to be used with arrow lenses, except snapshot best practices generally have
MutableState
objects hidden as implementation details.
m
I'd like to be able to pass a
MutableState
that is derived from another
MutableState
into a
@Composable
for read and write access. This has the same benefits as hoisting the state (https://developer.android.com/jetpack/compose/state#state-hoisting), but is more concise and more powerful (by being able to compose lenses). I don't think I understand how
mutableStateOf
is a lens right now. It can be written to, yes, but it isn't able to be derived from another
MutableState
as far as I know. And
derivedStateOf
has the opposite problem: it's able to be derived, but not able to be written to. Combining those aspects would make an extremely powerful FRP construct that I believe would make writing reusable and testable code much easier.
This is a more complicated example of what I'm trying to do. It works, but it's incredibly cursed code: https://github.com/magneticflux-/jetpack-compose-optics/blob/28a57e6ecf2b4abded227[…]c1919df0b86/src/main/kotlin/com/skaggsm/compose/lenses/prism.kt The loose "spec" I want that function in particular to follow is described in the commit:
This is the dual of
get
, i.e. the calling
State
is the one receiving optional focus of the
Prism
. If the focus can not be found, the resulting state is still updated until the calling
State
changes.
I'll get around to creating actual tests eventually, but it works in the bidirectional temperature conversion app I created.
a
I think the things that make that code cursed stem at least in part from the conceptual issues at the root of the goal here 🙂
In particular, this:
I'd like to be able to pass a 
MutableState
 [...] into a 
@Composable
 for read and write access.
is a thing you should not want to do. We spent a long time in this space in terms of the designs of Compose's state management and associated patterns, and this always led to dead ends and confused abstractions.
Mechanically of course you can do it, and it's often expedient when prototyping, but it creates confusing relationships between producers and consumers of the data in code that does it. Identifying the directionality of the data flow or the authoritative owner of the data becomes difficult.
When working with single values we recommend the
value: T, onChange: (T) -> Unit
pair of parameters instead because it sets expectations more clearly;
onChange
is a request; a polite signal of intent that may be considered and acted upon immediately, later, or not at all. In contrast almost any programmer would expect this test to pass:
Copy code
val first = foo.value
foo.value = expected
val second = foo.value
assertEquals(expected, second)
and similarly if you want a composable to control where in the composition that reads of values coming from its parameters happen,
() -> T
is always an option.
Specific hoisted state objects exist to set these expectations appropriately with their API shapes.
MutableState<T>
promises property access semantics that make it very easy to break other unidirectional data flow principles down the line
I would not be surprised if we added a lint warning against
@Composable
function parameters of type
MutableState<T>
at some point as a result.
And this is all before getting into the question of how
derivedStateOf
is being positioned above, which I think is its own topic entirely 🙂
m
Ah, I see where the confusion is happening: I'm trying to break unidirectional dataflow principles because I don't believe it's a productive way to look at reactive programming. In essence, I want to treat
MutableState<T>
as an "Atom" (borrowed from Clojure, used here https://github.com/calmm-js/documentation/blob/master/introduction-to-calmm.md#atoms). I believe that the "pair of parameters" style of hoisting state is redundant when a they can be combined into a single object: a lensed atom.
A similar approach is also taken in the Druid library for Rust, talked about in this blog post: https://raphlinus.github.io/rust/druid/2020/09/25/principled-reactive-ui.html