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?