Thanks everyone, I updated the proposal to clean up these issues below; I also have some replies further down:
In response to some other comments:
In order to do so, we would need to special case the type checker treatment of withGroup
and group.add
, to understand that their body closures are invoked only once, and that all add
-ed child tasks complete by the end of the withGroup
body. async let
may be a bit of a special case too, but it also cuts down on the withGroup
boilerplate, which a number of people pointed out is rather verbose for this common "pass a single value from a child task back to its parent" pattern. While we've discussed various "this closure runs only once" general annotations in the past, it is also less clear to me that the relationship between add
and withGroup
would be a generally useful thing for the compiler to understand, and it also isn't entirely static, because an add
may fail if a group has already been canceled.
Relative to Kotlin, I think the main difference in our model is that tasks are more strongly isolated, and that async functions are already strongly tied to executors via actors, so the task-executor relationship is more ephemeral. If a task touches no global or actor-bound state, then it generally doesn't matter what executor it runs on, and it will automatically hop to the appropriate actor executor if it does access shared state. Therefore, by default, I think it makes sense to avoid unnecessarily bottlenecking parallelism for the system by starting most new tasks in the global concurrent pool; they should automatically switch to more restricted executors when they need to.
This is an interesting idea; perhaps the add
method could optionally take a cancelOnGroupEnd:
argument.