I’ve been finding myself using this recently: ```i...
# arrow
p
I’ve been finding myself using this recently:
Copy code
internal 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
Copy code
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.
👍 1
s
Interesting operator, and definitely useful! I'm wondering here though if this usage of
CoroutineScope
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.
I think this operator might become redundant with the
resource { }
DSL though since then you can encode it in a more imperative way.
Copy code
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.
I'm very interested on your thoughts on that,
p
In my example, the
MyProcessor
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.
Another option for my
backgroundWorker
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
)
s
Right, that makes sense. In that case, I wouldn't pass the
CoroutineScope
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:
Copy code
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() }
  }
Another option for my
backgroundWorker
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
Like you said this is probably riskier