No idea what this should really be named; I'll tak...
# language-proposals
j
No idea what this should really be named; I'll take suggestions. For now I'm calling it "Monad Flattening". When using RxJava, I find that my team really struggles with thinking in terms of
Observable.map {}
and
Observable.combine()
, and I don't blame them. It's not particularly easy to read. Similar APIs and problems exist with Android's
LiveData
, Kotlin's own
Flow
, and several other similar libraries. I wonder if we can make operating on these types read similarly or identical to simpler code, much like suspension makes writing async code look like sync code. So here's the proposal, roughly:
Copy code
@MonadFlattening
interface Observable<T> {
    fun <OUT> map(transformer: (T)->OUT): Observable<OUT>
    fun <OUT> flatMap(transformer: (T)->Observable<OUT>): Observable<OUT>
    
    // For optimization purposes, not strictly speaking necessary
    fun <B, OUT> combine(other: Observable<B>, transformer: (T, B)->OUT): Observable<OUT>
}

fun sample(x: Observable<Int>, y: Observable<Int>) {
    // using & as a postfix operator to indicate the start of flattening.  Don't think that's the right symbol, but we'll work with it for now
    val onePlusX: Observable<Int> = 1 + x&
    // reduces to
    val onePlusX: Observable<Int> = x.map { 1 + it }
    
    val xPlusY: Observable<Int> = x& + y&
    // reduces to
    val xPlusY: Observable<Int> = x.combine(y) { x1, y1 -> x1 + y1 }
    
    val xAsString: Observable<String> = x&.toString()
    // reduces to
    val xAsString: Observable<String> = x.map { it.toString() }
    
    val xSlashY: Observable<String> = "${x&} / ${y&}"
    // reduces to
    val xSlashY: Observable<String> = x.combine(y) { x1, y1 -> "$x1 / $y1" }
    
    data class Point(val x: Int, val y: Int)
    val point: Observable<Point> = Point(x&, y&)
    val angle: Observable<Double> = atan2(y&.toDouble(), x&.toDouble())
}
This enables a better potential syntax for UI and data:
Copy code
val counter = Observable.interval(1, TimeUnit.seconds)
val view: TextView = TODO()
view::setText bind "${counter&} seconds elapsed"
Problem is that I think this is an incredibly large overhaul. It affects just about every expression in the language. I can see this enabling a lot of interesting ideas further down the line. I feel like there are some issues with the idea I'm not seeing yet, but this could be really useful.
y
What you're looking for are monad comprehensions. One simple yet powerful way to implement them would be to allow cloning of continuations generated by the compiler. An example of this can be seen here, where I implement monad comprehension stuff on top of delimited continuations on top of Kotlin's Continuations and some reflection to do the necessary copying
mind blown 1
j
There was something telling me in the back of my mind that this sounded related to multishot continuations
I'll have to play with that! Thank you!
m
Have you seen https://github.com/cashapp/molecule? While the syntax isn’t what you’re suggesting here, it’s the same concept I think. I would rewrite your example as follows:
Copy code
fun sample(xFlow: StateFlow<Int>, yFlow: StateFlow<Int>): Flow<Double> = moleculeFlow(mode = Immediate) {
    val x by xFlow.collectAsState()
    val y by yFlow.collectAsState()

    val onePlusX = x + 1

    val xPlusY = x + y

    val xAsString = x.toString()

    val xSlashY = "$x / $y"

    val point = Point(x, y)
    val angle = ant2(y.toDouble(), x.toDouble())

    return@moleculeFlow angle // you can actually return this and `moleculeFlow` will turn it into a flow again
}
👀 1
j
Fascinating; I developed something very similar for my KMP UI framework based on Solid.js. It results in a fairly similar API:
Copy code
val xPlusY = shared { x() + y() }
Mine used suspension instead of composability, mostly so you could also make API calls inline. I'm curious what the performance effects are; I'll be looking into this! My solution had some weird implications for identity and manipulating subscribers that I still dislike that proper monad comprehension wouldn't. I was hoping to have something that I could apply more directly to Rx that wouldn't rerun the previous parts of the block on change and allowed more convenient manipulations on the outer layer at the same time. I didn't think about Compose; I'll need to do some research on how it works.
e
under the proposed syntax, would
Copy code
(x& + y&).toString()
be equivalent to
Copy code
x.combine(y) { x1, y1 -> x1 + y1 }.toString()
or
Copy code
x.combine(y) { x1, y1 -> (x1 + y1).toString() }
? then how about
Copy code
listOf(1, 2).map { x& + it }
being either
Copy code
listOf(1, 2).map { x.map { x1 -> x1 + it } }
or
Copy code
x.map { x1 -> listOf(1, 2).map { x1 + it } }
?
1
j
Lol, that's a good question. The proposed syntax is not workable that way, I suppose - it isn't clear on where the "magic" ends. Of course, this whole discussion may be completely unnecessary with the two responses above.
Originally I wasn't even considering using a symbol at all, which was even worse. Vagueness on beginning and ending.