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 abackgroundWorker
and require the caller to wrap it in asuspend fun
in their own scope if they wished, but then there's nothing linking the scope lifecycleslaunch { }