Warning: possible noob question<<<
Hi There! I was playing with Flow class and couldn’t find a way to pass to the transform function an object. Something along the lines of
val letters = ('a'..'f').toList().toTypedArray()
val lettersFlow = flowOf(*letters)
var transformer = object : suspend FlowCollector<Pair<Char,Char>>.(value: Char) -> Unit {
var oldVar: Char = 'x'
override fun invoke(collector: FlowCollector<Pair<Char,Char>>, value: Char) {
collector.emit(Pair(oldVar, value))
oldVar = value
}
}
val result = lettersFlow.transform(transformer).drop(1).toList()
Which obviously doesn’t work.
Basically the idea is to pass an object with state instead of a lambda.
The question is: can it be done? How? Why if it’s not possible?
A lambda is not so different from an anonymous object. Any var the lambda references effectively becomes part of the state of the lambda. Also, you can just define a lambda instead of inheriting it.
This is equivalent to what you are trying to accomplish
val letters = ('a'..'f').toList().toTypedArray()
val lettersFlow = flowOf(*letters)
var oldVar: Char = 'x'
var transformer: suspend FlowCollector<Pair<Char,Char>>.(value: Char) -> Unit = { value ->
emit(Pair(oldVar, value))
oldVar = value
}
val result = lettersFlow.transform(transformer).drop(1).toList()
For readability, I’d recommend defining an extension method on Flow for this kind of thing.
Also, methods like scan and reduce are useful for keeping state while progressing through a Flow.
Example:
val result = lettersFlow
.scan<Char, Pair<Char, Char>>('x' to 'x') { acc, item -> acc.second to item }
.drop(2)
.toList()
Hi Nick,
Thanks for your answers. In my opinion, the difference between a lambda and an object that implements a function type is encapsulation, which is one of the pillars of OOP. So basically the point is to have ‘oldVar’ only accessible from the ‘lambda’. An extension function would allow me to encapsulate member variables, as in this example: zipWithNext for Flow · Issue #1767 · Kotlin/kotlinx.coroutines · GitHub but doesn’t allow me to write that in one go using dot-chaining (which in the end is the point of lambda: writing the anonymous function where you actually use it without defining it). Plus I’m not sure about visibility of extension functions, are they visible globally or have limited scope?
Regarding your solution with scan, that what i posted on S.O. as possible answer of my question: zip with next value with kotlin Flow - Stack Overflow
It’s a viable solution and what i’m using now in my code, but I’m interested in understanding how to implement more general transforms and what pattern to use. I find using operators quite limiting and I cannot wrap my mind around doing some simple things, so I’m looking to a more general way of tackling more complex problems.
Lambdas are absolutely awesome for encapsulation. Notice here how nothing can use oldVar
except the transform lambda because it’s declared in the run lambda:
val letters = ('a'..'f').toList().toTypedArray()
val lettersFlow = flowOf(*letters)
val result = lettersFlow
.run {
var oldVar: Char = 'x'
transform {
emit(oldVar to it)
oldVar = it
}
}
.drop(1)
.toList()
Then why isn’t oldVar private in your example ? You can’t accidentally forget the modifier for properties defined in a lambda.
An extension function is just a regular function with fancy syntax for the first parameter. Same rules apply so encapsulation works the exact same way:
class A
class B {
private fun A.doStuff() {} //Only accessible inside B
}
You’ve already linked to the “more general way” of dealing with Flow
. Define your own operator as an extension method on Flow using the flow
builder function. Any state defined within the flow lambda is private to that instance of the flow that will be created. Then you make that extension function as widely or narrowly available as desired.
You can make it a private member function of the class where it’s used:
class Example {
private val letters = ('a'..'f').toList().toTypedArray()
private val lettersFlow = flowOf(*letters)
suspend fun fetchResult() = lettersFlow.pairWithNext().toList()
private fun <T> Flow<T>.pairWithNext(): Flow<Pair<T, T>> = flow {
var prev: Any? = UNDEFINED
collect { value ->
if (prev !== UNDEFINED) emit(prev as T to value)
prev = value
}
}
private object UNDEFINED
}
You can even embed it right into the method where it’s used. I don’t recommend it except for the most trivial functions for readability but it’s very encapsulated:
val letters = ('a'..'f').toList().toTypedArray()
val lettersFlow = flowOf(*letters)
fun <T> Flow<T>.pairWithNext(): Flow<Pair<T, T>> = flow {
var undefined = Any()
var prev: Any? = undefined
collect { value ->
if (prev !== undefined) emit(prev as T to value)
prev = value
}
}
val result = lettersFlow.pairWithNext().toList()