joseph_ivie
06/11/2024, 4:33 PMObservable.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:
@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:
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.Youssef Shoaib [MOD]
06/11/2024, 5:47 PMjoseph_ivie
06/11/2024, 5:49 PMjoseph_ivie
06/11/2024, 5:50 PMmolikuner
06/11/2024, 5:51 PMfun 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
}
joseph_ivie
06/11/2024, 6:20 PMval 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.ephemient
06/11/2024, 7:46 PM(x& + y&).toString()
be equivalent to
x.combine(y) { x1, y1 -> x1 + y1 }.toString()
or
x.combine(y) { x1, y1 -> (x1 + y1).toString() }
?
then how about
listOf(1, 2).map { x& + it }
being either
listOf(1, 2).map { x.map { x1 -> x1 + it } }
or
x.map { x1 -> listOf(1, 2).map { x1 + it } }
?joseph_ivie
06/11/2024, 8:03 PMjoseph_ivie
06/11/2024, 8:04 PM