A syntax-level support for reactive ref type

A reactive pipeline is very commonly used in various sicarios. In frontend development, we use such pipelines to bind the states and views in many ways. For example, we have ref and computed in Vue. We have re-executing mechanism in React and Compose. In server side development, we use streams and flows to process sequential data.

However, to define the reactive pipeline is not that intuitive. For example, to define c=a+b, we have to write a lambda with explicit specifications. The lambda thing prevent us to write complicated pipelines conveniently.

val a_flow:Flow<Int> = flowOf(1, 2)
val b_flow:Flow<Int> = flowOf(10, 20)
val c_flow:Flow<Int> = a_flow.zip(b_flow) { a, b -> a + b }

I believe that it can be much better if we provide a syntax-level support. To be short, the syntax allows us to write reacive pipelines intuitively (just a+b) as following.

var a_ref: var ref<Int> = 1
var b_ref: var ref<Int> = 10
val c_ref: val ref<Int> = a + b

unref(c_ref) // 11

a_ref = 2
b_ref = 20
unref(c_ref) // 22

Here I introduced ref types which are something like pointers in C, references in C++ or Ref in Vue. The code can be compiled in desugaring stages into the following.

val a_ref: VarRef<Int> = VariableRef(1)
val b_ref: VarRef<Int> = VariableRef(10)
val c_ref: ValRef<Int> = ComputedRef(a_ref, b_ref) { a, b -> a + b }

c_ref.get() // 11

a_ref.set(2)
b_ref.set(20)
c_ref.get() // 22

The compiler automatically converts all operations applied on ref values. Specifically, here “all operations” means being used as an argument in any functions. In this cases function add(a: Int, b:Int).

Functions can also mark its arguments or return value as a ref, which allows it to accept the reference itself but not the referred value.

fun addPipeline(a_ref: val ref<Int>, b_ref: val ref<Int>): val ref<Int> {
    return a+b;
}

When calling a function, a reference (ref<Int>) and be cast into a value (Int) making it returns a computed reference. (It can be nested)

When assigning to variable or argument, a value (Int) can be cast into a constant reference (val ref<Int>) for convenience; a mutable reference (var ref<Int>) can be cast into an immutable reference (val ref<Int>).

A series of special functions are intorduced to bridge the references and desugared objects. So that we can easliy operate a reference as an object, vise versa.

fun wrap(a: val ref<Int>): ValRef<Int>

fun wrap(a: var ref<Int>): VarRef<Int>

fun unwrap(a: ValRef<Int>): val ref<Int>

fun unwrap(a: VarRef<Int>): var ref<Int>

fun <T> unref(a: val ref<T>): T = wrap(a).get();

We can define custom subclass of ValRef and VarRef to define how the values are computed or assigned.

Futher more, we can introduce the suspend val ref<Int> to allow waiting/watching the value changes asynchronously.

And it make it possible to perform optimizations at the compiler level (like fusing several operations into one lambda).

What do you think about this idea?

Looks like atomics and operator functions? I’m not really understanding what you’re proposing here, or how it helps. Can you provide some concrete examples, of something you would do in the front end using Vue, and then how you would like to do the same thing in Kotlin, and why you can’t do it currently?

You can define an operator plus function to add two Flows together, to prevent having to call zip.

3 Likes

There is already a plus operator function that appends one flow to another.

But the original example can be shortened using Int:plus:

    val c_flow: Flow<Int> = a_flow.zip(b_flow, Int::plus)

I too do not understand what exactly was being proposed and how it helps

It looks like his VarRef and ComputedRef can all be shortened to:

    var a_ref = 1
    var b_ref = 10
    val c_ref: Int get() = a_ref + b_ref
    c_ref // 11

    a_ref = 2
    b_ref = 20
    c_ref // 22
1 Like

I think what you’re looking for are monad comprehensions, or in general, direct-style effects.
I have a library that enables such a use-case. Your example would thus look like:

val aFlow = flowOf(1, 2)
val bFlow = flowOf(10, 20)
val cFlow = runFlowCC { aFlow.bind() + bFlow.bind() }

Molecule also does this, but only for StateFlows, using some Compose magic. It’s very useful for StateFlows, but doesn’t have the full generality of my library.
Effect interaction is especially of great importance here. The full generality of effects allows for incredibly intricate programs to be written succinctly. I have truly marvelous examples of this, which this forum post is too narrow to contain…

In general though, your idea seems to boil down to StateFlows, and for that, Compose and Molecule do an incredible job already, so I’d really suggest you look at those.

2 Likes