Task Groups (creation) design choices of API

Hello there.

I've been reading-up on Swift Concurrency again to reinforce my knowledge about how it works.

The Swift Book chapter is very good and clear.

However, when I get to the Task Groups section the API seems a little bit odd (maybe just to me though...)

Specifically, when creating a TaskGroup one uses the 'free' function withTaskGroup(of:returning:body:)

1 - Why is this a free function and not a Type method on Task? I see that in the initial pitch that it was. This seems unlike other API design in Swift.

2 - Is it just me or does the name of the function seem odd?
withTaskGroup {} do what with the taskGroup? A free function that starts with 'with' seems odd to me. Does it to anyone else? Is this a side-effect of the change from being a Type method to becoming a free function? (Task.withNursery() sounds better than the solitary withTaskGroup())

(Obviously this is post-event bike shedding, so of little practical use, but I'm curious.)

Thanks.

It must be a "with..." style API because as you leave the scope the group is guaranteed to be destroyed and cleaned up all its inner tasks and resources.

We don't have the necessary language features to express some form of "start here; run some cleanup at end of scope" RAII style things in Swift today. This may change in the future, but today all such APIs use with... style, the same is true for withUnsafeBytes, TaskLocal's withValue etc. Specifically, it is not safe to escape the group parameter outside of this closure. We would want to make this parameter non-escapeable but this also is a feature that did not exist as the Concurrency APIs were first designed, we could perhaps make it so in the future.

The "with" enforces the structured concurrency nature of groups.

Nursery name was abandoned very quickly, during reviews, so I don't think there's much to rehash here -- you can browse all Swift Evolution review discussions if you want.

I'd recommend giving this talk a watch, it explains how structured concurrency works with some practical examples: Beyond the basics of structured concurrency - WWDC23 - Videos - Apple Developer perhaps this will help.

4 Likes

Ahh, thanks. I have not used the unsafe functions, so I'm not familiar with the existing 'with' naming style.
Thanks.

1 Like

In case anyone else is interested. The rationale for moving the TaskGroup creation function out from Task to a top level function is listed as:

  • Move Task.withCancellationHandler to a top level function withTaskCancellationHandler which reads more logically, as it does not create a task by itself.

From SE Proposal 0304 - revision history

That's about the cancellation handler installing method, but yeah in general same rationale applies :)

That'll teach me to skim read the document too quickly!

I think it's worth mentioning here that this is not the case for withUnsafeContinuation which is... odd.

1 Like

I don't know the actual lineage, but I like to pretend, at least, that the withXYZ style is mimicking Python's with keyword. Swift's approach isn't as elegant but it's somewhat equivalent, functionally - if you're very careful about how you implement the withXYZ function.

As a fun side effect of borrowing pattern matching, you can combine an unconditional if case borrowing pattern match with a _read coroutine to implement scope resource acquisitions in a way that's closer to what Python supports:

struct Resource: ~Copyable {}
var resource: Resource {
  _read {
    print("acquiring")
    yield Resource()
    print("releasing")
  }
}

if case _borrowing r = resource {
  print("using")
}
9 Likes

An async continuation would be useless if the language didn't allow it to escape local analysis; any code that could obey those restrictions could always be rewritten to just return normally. However, the continuation value is still scope-bound like any other with API: once withUnsafeContinuation has returned, the continuation is useless (and in fact points to deallocated memory, in the case of UnsafeContinuation). So it's not actually an exception.

1 Like

That's a neat trick, but it makes it seem like we're so close - it just makes me want a real with statement even more. :stuck_out_tongue_closed_eyes:

1 Like