To be fair, this type is already going to be hard to use even without ~Discardable, simply because it's non-copyable.
In your actor example, continuation is an Optional. Because of this, we are essentially just shifting the responsibility of the dynamic check for a double-resume to the user. When we call continuation.take(), we have to make a deliberate decision on how to handle an unexpected nil - whether to force-unwrap, use a precondition or an assert, log to analytics, or silently ignore it.
It feels very asymmetric that the responsibility for handling >= 2 resumes is pushed onto the user, while the responsibility for handling 0 resumes is hardcoded into the type via a deinit trap, giving the user absolutely no degree of control*.
Thinking about it more, my own reasons against ~Discardable actually come down to how it would compose with the rest of the language. Non-discardability is viral (an Optional of a non-discardable type becomes non-discardable), but we would still need a definitive point of destruction for these values inside classes and actors. The only viable places to safely consume a non-discardable property are where we know the enclosing object is dying and no further methods can be called on it - in deinit or a consuming method that discards self. To solve this, we would need an entirely new set of rules for deinit:
actor MyStateMachine {
var continuation: Continuation<...>?
deinit {
// The compiler must enforce special handling of 'continuation' here
if let continuation = consume continuation {
continuation.resume(...)
}
// The compiler should also prevent calls requiring 'self' at this point,
// because we consumed a part of it.
}
}
And this feels to me like a lot of extra language complexity (and even more implementation complexity) for a yet-to-be-defined win. But I'd still prefer to have this properly explored before making a decision about this proposal.
Sure, I agree, and this pitch is arguably a good solution. But a solution with tradeoffs (a whole spectrum of them) can be written by anyone in their code today. The proposed version is functionally no different than those user-space wrappers.
And if this is just a good-enough tradeoff, what is the urgency to lock it into the standard library right now (with such a cool name)?
* We can give the user control over how to handle an unused continuation, while keeping a reasonable default behavior:
public protocol UnusedContinuationHandler {
func handle()
}
public struct DefaultUnusedContinuationHandler: UnusedContinuationHandler {
public init() {}
public func handle() { fatalError() }
}
public typealias Continuation<Success, Failure: Error> =
NonCopyableContinuation<Success, Failure, DefaultUnusedContinuationHandler>
public struct NonCopyableContinuation<Success, Failure: Error, UnusedHandler: UnusedContinuationHandler>: ~Copyable { ... }
@inlinable
public nonisolated(nonsending) func withContinuation<Success, UnusedHandler: UnusedContinuationHandler>(
of type: Success.Type = Success.self,
unusedHandler: UnusedHandler,
_ body: (consuming NonCopyableContinuation<Success, any Error, UnusedHandler>) -> Void
) async throws -> Success {
try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation<Success, any Error>) in
body(NonCopyableContinuation(continuation, unusedHandler))
}
}
@inlinable
public nonisolated(nonsending) func withContinuation<Success>(
of type: Success.Type = Success.self,
_ body: (consuming Continuation<Success, any Error>) -> Void
) async throws -> Success {
try await withContinuation(of: type, unusedHandler: DefaultUnusedContinuationHandler(), body)
}
This at least removes the asymmetry.