on the Jan 30 nightly build and I'm confused about the meaning of marking an argument of function type as being both @escaping and consuming.
Consider the following code:
public struct Continuation<T: ~Copyable, R>: ~Copyable {
public let run: (consuming @escaping (consuming T) async -> R) async -> R
public init(_ run: @escaping (consuming @escaping (consuming T) async -> R) async -> R) {
self.run = run
}
init(_ t: consuming T) {
run = { (callback: consuming @escaping (consuming T) async -> R) async -> R in await callback(t) }
}
}
The second init fails to compile with:
Noncopyable 't' cannot be consumed when captured by an escaping closure
This would seem to preclude passing functions that consume ~Copyable arguments as closures full stop. Am I reading that right?
The only way I see around that is to treat the callback argument as an actual non-copyable type and say that consuming it entails invoking it exactly once. I'd be curious as to the thoughts of those actually working on this stuff.
For comparison, this does come up sometimes in Rust, and the workaround is to put the non-copyable value in an Option(al), so you can assert at run time that it’s only consumed once in practice. (Rust does have closures that can only be called once, but they don’t always fit into all API requirements, so enforcing this bit at run time isn’t unheard of.)
To be clear, consuming here has no effect on the type of the value, which is still just a regular escaping, copyable, multiply-invocable closure value. It's only a parameter modifier that causes the closure value to be passed callee-release while inhibiting implicit copies (but still allowing explicit copies) in the callee.
Certainly understand that, but FWIW (admittedly not a lot), I keep running into situations where I'd really like the behavior that @jrose mentions above in Rust where arguments of function type which are marked consuming would require exactly one invocation.
Most notably, having the compiler enforce SE-300's frequently reiterated requirements:
As long as the entirety of the process follows the requirement that the continuation be resumed exactly once, there are no other restrictions on where the continuation can be resumed.
by treating noncopyable function types in a consistent manner with noncopyable structs and enums, coupled with, say, a version of AsyncStream.yield that was consuming, would save me from creating a lot of leaked tasks that I keep finding myself inadvertently writing.
If T: ~Copyable, then it is correct that it can't even be explicitly copied. Using the capture list without the copy should still move ownership of the value from the enclosing scope into the closure:
run = {[t = consume t] (callback: consuming @escaping (consuming T) async -> R) async -> R in await callback(t) }
However, because the closure itself can still be escaped, retain/released, and run multiple times, t still cannot be consumed by the body of the closure.
I agree one-shot functions would be useful, but they would have to be a new kind of function type. In Rust terms, our nonescaping closures are like Rust's Fn trait, borrowing their enclosing context, and our escaping closures are akin to Arc<Cell<dyn FnMut>>, but we don't have FnOnce yet.