phldavies
04/28/2022, 11:40 AMinternal infix fun <A, B> Resource<A>.flatTap(resource: (A) -> Resource<B>): Resource<A> =
flatMap { a -> resource(a).map { a } }
which allows for constructs such as
fun CoroutineScope.myProcessor(database: Database) = resource {
MyProcessor(coroutineContext, database)
} release {
it.close()
} flatTap {
resource { it.backgroundWorker() } release Job::cancel
}
Effectively allowing for similar usages as tap {} but with a teardown/release.simon.vergauwen
04/28/2022, 3:59 PMCoroutineScope is safe but it's hard to say what is launching where.
MyProcessor inject the coroutineContext from the CoroutineScope into its constructor.
backgroundWorker returns a Job but not sure where it is coming from, it looks like MyProcessor can have it's own CoroutineScope but then there is no reason for the CoroutineScope on the myProcessor function.simon.vergauwen
04/28/2022, 4:08 PMresource { } DSL though since then you can encode it in a more imperative way.
private fun myProcessor(database: Database) = resource { MyProcessor(coroutineContext, database) } release { it.close() }
private fun MyProcessor.worker() =
resource { it.backgroundWorker() } release Job::cancel
fun processor(datbase: Database) = resource {
myProcessor(database).bind().also {
it.worker().bind()
}
}
Although this code is a bit more redundant than your original example, it becomes nicer if you make the constructor private, and expose a factory method that returns Resource<MyProcessor>. Same for backgroundWorker() if you make its return type Resource<...> such that it cannot be consumed incorrectly.simon.vergauwen
04/28/2022, 4:09 PMphldavies
04/28/2022, 4:26 PMMyProcessor encapsulates it's own CoroutineScope for it's lifetime - the backgroundWorker Job belongs to that inner scope and is cancelled along with it on close, but only after a timeout (so we don't really need the flatTap here as just launching it would be enough, but cancelling it directly is better than waiting for the timeout).
The CoroutineScope on the myProcessor is to actively provide the outer coroutineContext to the inner scope as a parent. Without it (or without passing a context as a parameter) the coroutineContext global suspend val will be used (as in the case of your example) and the inner-launched Job will hold the resource { } block open (as the nested NonCancellable scope will be the parent of the inner-launched Job)
I've seen the new resource { } DSL and will be looking forward to trying it but I haven't yet made the switch to 1.1.x. Certainly I prefer the look of it for resource composition.phldavies
04/28/2022, 4:29 PMbackgroundWorker was to just make it a suspend fun and require the caller to wrap it in a launch { } in their own scope if they wished, but then there's nothing linking the scope lifecycles - would just need to be careful about the implementation of backroundWorker then I suppose, to ensure it can cater for the MyProcessor scope to be cancelled (maybe a check to MyProcessor.innerScope.isActive)simon.vergauwen
04/29/2022, 8:26 AMCoroutineScope though.
It'll inherit the coroutineContext from where you call .use { ... } and if you'd want to parameterize that I would use coroutineContext: CoroutineContext? = null.
The final code could look something like this:
import arrow.fx.coroutines.ExitCase
import arrow.fx.coroutines.Resource
import arrow.fx.coroutines.continuations.resource
import arrow.fx.coroutines.release
import arrow.fx.coroutines.releaseCase
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
object Database
class MyProcessor private constructor(
coroutineContext: CoroutineContext,
database: Database
) {
private val scope = CoroutineScope(coroutineContext)
suspend fun close(exitCase: ExitCase) {
scope.cancel("Closing MyProcessor Scope: $exitCase")
}
fun myBackgroundWorker(): Resource<Job> =
resource {
scope.launch { delay(Long.MAX_VALUE) }
} release Job::cancel
companion object {
operator fun invoke(coroutineContext: CoroutineContext, database: Database): Resource<MyProcessor> =
resource { MyProcessor(coroutineContext, database) } releaseCase { p, ex -> p.close(ex) }
}
}
fun myProcessor(database: Database, coroutineContext: CoroutineContext? = null): Resource<MyProcessor> =
resource {
MyProcessor(coroutineContext ?: currentCoroutineContext(), database).bind()
.also { it.myBackgroundWorker().bind() }
}simon.vergauwen
04/29/2022, 8:26 AMAnother option for myLike you said this is probably riskierwas to just make it abackgroundWorkerand require the caller to wrap it in asuspend funin their own scope if they wished, but then there's nothing linking the scope lifecycleslaunch { }