Closures in API design – theoretical limitations and best practices?

In async, with the call chain ABA' where A is polling B, B is an async fn, and A' is an async callback, then A' can collaborate with A to suspend B and resume it whenever it wants, or cancel (drop) it. In fact, this is exactly what happens any time B uses an async sleep or IO operation.

However, this is still not arbitrary continuations, because arbitrary continuations allow you to resume B twice. This is both why they are extremely powerful and why they make it hard to reason about code. This is not possible in Rust — unless B is a type that implements Future + Unpin + Clone, in which case you could clone it and resume both clones! There may or may not be some general continuation pattern that still can't be expressed; I'm not sure.

Yes. This is a general property of language design: if you add more fundamental operations or permitted states, you remove properties that other people writing code, or reading code, might want to rely on. Every language occupies a unique set of tradeoffs in this space, and this is part of why there are so many programming languages; there can never be one “best” language.

Also, an unsolved hard problem here is writing language specifications (or even documentation) that define a useful and yet not overly strict set of things that the language won't do in future versions. This is important whenever the language contains callbacks or generics, and seeks to make it easy or feasible to write code that is correct under all circumstances (which Rust does; this is among other things the concept of “is this unsafe code in a safe function sound?").

Historical example: Rust used to offer a guarantee that all values would be Dropped before they went out of scope; this would have allowed things like replace_with and scoped threads to be implemented purely in terms of droppable values rather than callbacks. However, it turned out to be infeasible to actually implement that guarantee (because of, among other cases, Rc/Arc cycles), and so it was removed (and std::mem::forget() added). If this inconsistency had been discovered after Rust 1.0 stability rather than before, it would have been a disaster for the correctness of code already written.

1 Like