In the similar vain to Copyable and Escapable, I suggest the addition of SyncConsumable, an implicit interface that indicates a value of a type may be consumed in a context that is not async.
"Consumed" refers to any operation that may dereference a class or call deinit on a non-copyable struct. This may include:
- Defining a local variable and letting it go out of scope without capturing it or passing it to a consuming function
- Receiving a value as a non-borrowing parameter, and letting it go out of scope without capturing it or passing it to a consuming function
- Using the
consumeoperator directly - Assigning a value to class or struct member or global variable of the type, overriding the previous value
- Having a class or struct member of the type. The containing class or struct is considered a "sync context" if it implements
SyncConsumable.
When a type does not implement SyncConsumable, no operation similar to the above is allowed in a context that is not async.
In exchange, such a type may use await in its deinit, if it is allowed to have deinit, i.e. if it is a class or a ~Copyable struct.
Use cases
There are many cases where being able to await inside a deinit is useful. The following are just a select few.
Certain stream formats, like gzip or AES-XTS need to be able to perform a "final write" after the final data has been received. In a sync context, it's possible to create a "gzipped file" class that receives regular writes, and automatically adds the final write when it is closed. It can also guarantee being closed when going out of scope, removing the chances of a leaking the resource, or leaving a corrupt file. Without being able to await inside deinit, it's not possible to translate this pattern to async, however.
AsyncIteratorProtocol does not have an explicit cancel, break, or return function, to indicate no more elements are desired. If it's drawing elements from some resource, that resource needs to be cleaned up in the deinit function. However, this is currently only possible if the clean up does not, itself, require using await.
A class may want to keep a background task running during its lifetime. On deinit, it should cancel the task, but it should also await it to ensure cancel processed correctly, and no resources were leaked. Currently, the closest option is to expose the background task (or have the class inherit from Task), and async let it from the context where the class is created. This creates room for error, if one forgets to async let, as not leaking the task would not be guaranteed at compile time.
Adding to both of the above, this can be used to create generators. The generator class would simply create a background task that yields elements, and then awaits until the next element is requested. On deinit, a signal needs to be sent to the background task to cleanly close, and then it must be awaited. A simple cancel may prevent cleanly releasing any held resources.
It could also be used for wrapping withTaskGroup in a class that's ~Copyable and ~SyncConsumable, thereby removing the need for a closure. This could allow, for example, to use a task group from inside a loop, and still be able to break out of said loop, which would result in deinit being called on the class containing the task group. This would, in turn, cause all tasks in the group to be awaited.