It's similar to explicit DI vs implicits.
the function coloring metaphor doesn't make sense since the calling convention is the same nor are there extra function keywords (`async` vs non-async).
A function without a context can still be used just fine from any code, you just won't be able to cancel it.
However, you raise a different point:
The tasks I'm running don't involve the network, they either succeed or error after an expensive calculation.
This sounds like CPU-bound, not I/O-bound. (Please correct me if I misunderstand.) Can you please confirm if you are using Go or a different language? If Go, I guess it still makes sense, as green threads are preferred over system threads. If not Go, I would be nice to hear more about your specific scenario. HN is a great place to learn about different use cases for a technology.You can always pass context.Background, in this metaphor creating a new tree of color.
You can always call "runtime.block_on(async_handle)", in this metaphor also creating a new tree of color.
Say you have a foreach function that calls each function in a list. In async-await contexts you need a separate version of that function which is itself async and calls await.
With context you can pass closures that already have the context applied.
You're talking about doing this:
f1 := func() error { return nil }
ctx_f1 := func(context.Context) error { return nil }
fns := []func() error{f1, func() error { ctx_f1(ctx) }}
Basically, writing an anonymous conversion function.In, say, rust, the equivalent in this analogy would be:
let f1 = || -> Result<(), ()> { Ok(()) };
let async_f1 = async { || -> Result<(), ()> { Ok(()) } };
let fns = vec![f1, runtime.block_on(async_f1)];
That conversion function, 'runtime.block_on', doesn't really seem that different. The analogy still seems to hold.For a more concrete example: let's say you have a generic function that traverses a tree. You want to compare the leaves of two trees without flattening them, by traversing them concurrently with a coroutine [1]. AFAIK in rust you currently need two versions of traverse, one sync one async as you can't neither close over nor abstract over async. In go, where you have stackful coroutines, this works fine, even when closing over Context.
So yes, in some way Context is a color, but it is a first class value, so you can copy it, close over it and abstract over it, while async-ness (i.e. stackless coroutines) are typically second class in most languages and do not easily mesh with the rest of the language.
[1] this is known as the "same fringe problem" and it is the canonical example of turning internal iterators into external ones.