I'm +1 on this proposal, this is clearly important functionality, and I appreciate that the API works with both error handling and Result
types, that is a nice touch. Some more detailed thoughts:
This point in the alternates considered section is a bit concerning/confusing to me:
- Although the consequences of misusing
CheckedContinuation
are not as severe as UnsafeContinuation
, it still only does a best effort at checking for some common misuse patterns, and it does not render the consequences of continuation misuse entirely moot: dropping a continuation without resuming it will still leak the un-resumed task, and attempting to resume a continuation multiple times will still cause the information passed through the continuation to be lost. It is still a serious programming error if a with*Continuation
operation misuses the continuation; CheckedContinuation
only helps make the error more apparent.
Why does this leak a task? Is this a logic error like a leak or is this a memory safety violation? If this is a memory safety problem, then this should probably also be an unsafe API, even if it is slightly safer. If it is memory safe but "just" a logic error, then I don't think the rationale above is super strong. We have lots of logic errors that happen from API misuses, and we don't dance around that. For example, "Sequence" in a generic context is an unchecked single iteration sort of beast and is often misused, we don't encode fear into the name.
I really wish we could simplify this a bit, by merging UnsafeContinuation
and CheckedContinuation
into a single safe XYZContinuation
type. Per Konrad's nice explanation above, this would introduce some overhead, but:
-
These types are only used when interoperating with legacy completion handler APIs that should eventually be replaced with more SwiftConcurrency-native centric APIs over time.
-
It might be possible to reduce the API surface area by factoring this in other ways.
Just to brainstorm other factorings, instead of duplicating the struct types, would it be possible to have these two functions (plus the corresponding throwing versions):
func withContinuation<T>(_ operation: (XYZContinuation<T>) -> ()) async -> T {}
func withContinuation<T>(unsafe: Bool, _ operation: (XYZContinuation<T>) -> ()) async -> T {}
Notably the first operation would always be "checked", the later would be optionally safe:
// proposed
withCheckedContinuation { continuation in
..
}
withUnsafeContinuation { continuation in
..
}
// Alternate:
withContinuation { continuation in
..
}
withContinuation(unsafe: true) { continuation in
..
}
This uses one struct type for the checked and unsafe cases (the unsafe one would just use a null ARC pointer so no significant overhead would be present). The makes the types and operations "safe by default" and has less API surface area. Again, you'd still need the "throwing" versions of them.
The rationale in the alternates considered section about wanting to avoid the Continuation
name for the type makes a certain sense to me (I also can't wait for move-only types and deinit on structs!):
- Naming a type
Continuation
now might take the "good" name away if, after we have move-only types at some point in the future, we want to introduce a continuation type that statically enforces the exactly-once property.
Would it be possible/reasonable to do something like:
public struct XYZContinuation ...
public typealias Continuation = XYZContinuation
and then in a future release you can roll out NonCopyableContinuation
and change the typealias possible with a new Swift version. This could work if the patterns people use in practice don't involve copies of the continuation in the common case.
I just throw these things out as ideas -- I don't know how practical they are, but it would be good to consider them. Overall, very nice and understandable proposal, thank you!
-Chris