if I have a `CoroutineScope` with a `suspend` meth...
# coroutines
m
if I have a
CoroutineScope
with a
suspend
method that calls any
fun CoroutineScope.function()
...
Copy code
class MyClass: CoroutineScope {
  override val coroutineContext = SupervisorJob()

  suspend fun myFunc() {
    someSuspendFunction()

    launch { } // warning! Ambiguous coroutineContext!
  }
}
I get the warning
Ambiguous
coroutineContext
due to
CoroutineScope
receiver of suspend function
I don't understand what's ambiguous about it? `myFunc`'s context should be defined by... wherever it's called from, and the block's context for
launch
should be defined by
this
(an instance of
MyClass
), shouldn't it? I also don't understand why adding a
run
removes the warning
Copy code
run {
    launch { }
}
or calling another method that calls
CoroutineScope.launch()
Copy code
fun myLauncher() = launch { }

// ...

suspend fun myFunc() {
  someSuspendFunction()

  launch { }
}
I've read that extending
CoroutineScope
is often avoided, and that's fine, but I'd still want to know what's going on here. I also came across a blog post by Roman Elizarov talking about Explicit concurrency, but I want to launch-and-forget coroutines right after calling a suspend function.
c
There are a few issues with this, but the main one is that this flow is breaking with the conventions of “structured concurrency”. At a high level, Structured Concurrency means that each
launched
coroutine inherits from its parent context, and thus cancellation can error handling is able to follow that hierarchy naturally. So when looking at this code, you need to follow the coroutine hierarchy, and you’ll notice that it’s not doing this.
myFunc
is marked
suspend
, which means that it is going to be running on the
CoroutineScope
of whoever calls that function. The “natural parent” of
myFunc
is not
MyClass
, but is the coroutine that called
instanceOfMyClass.myFunc()
. With that in mind, when you try to run
launch
within myFunc, it needs to be launched on a
CoroutineScope
. Its signature is
CoroutineScope.launch
, but this is more than simply needing any
CoroutineScope
as the receiver. The expectation is that, since your calling it from a
suspend
function, that the
launch
function is related to the parent CoroutineScope of the suspend function. But in this case it is not. Since
MyClass
implements
CoroutineScope
and overrides the
coroutineContext
,
launch
now has
MyClass
as a receiver with which to launch upon. But
MyClass
is not related to the parent CoroutineScope of
myFunc
in any way, so it would not be able to cooperate with cancellation or anything else. And this is why you’re getting a warning. It technically would be able to launch, but it’s pretty clear that this is a bad pattern. When one reads the code, they would expect it to do one thing (be related to
myFunc
) but in reality it’s just running on it’s own. Because the class implements
CoroutineScope
, much of this logic becomes implicit and hard to follow. This warning comes because this is a common antipattern, of attempting to
launch
from a
suspend
function. But when you wrap it in
run
, as far as the compiler sees, that
launch
block is no longer directly within a
suspend
function, so the warning goes away. But that doesn’t mean that it fixes the problem. So like you mentioned, a better way to go to launch a “fire-and-forget” task as the end of
myFunc()
is to be explicit about it. Don’t implement
CoroutineScope
, but instead hold onto a
coroutineScope
property so that you’re forced to do
coroutineScope.launch
, which makes it explicit that the launched code is not related to
myFunc
m
I see I see. So it's ambiguous not to the compiler, but to the users, at first glance. Also, do you know if having a
private
scope is also an anti pattern? I like how jobs propagate exceptions and also how you can cancel an entire scope, it makes everything work tightly as long as every part is willing to cooperate... but I also think there might be cases where I might want to have a less-flexible library where some `Job`s are not in reach at all (and the user might need to call my custom
.stop()
or whatever to cancel the scope and everything else.) Mostly out of laziness, though, because these `Job`s be designed to catch cancellations and clean up (or callback to clean up) by themselves (when I say "clean up" I mean, in my case, I'm actually keeping track of these jobs, through a map and a few methods. My state is a bit all over the place)