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
consume
operator 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 await
s 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 await
ed. 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 await
ed.