I'm writing a program that needs to do some long running disk I/O while being responsive to user input. I am thinking about making heavy use of a type like
class Pending[T] {
def get(): Option[T]
}
to represent the result of a long running computation. get() returns Some[T] if the operation is done, None otherwise. But each time None is returned, it makes a little progress in the computation. Thus a thread can alternately call get() and check if there is any user input, so it can quickly respond to new user input.
(Apologies for Scala style pseudocode. Still a Rust learner. I do intend to write the actual program in Rust)
class PendingIterator[T] {
def next(): Option[T]
}
represents a sequence of long running computations. next() returns Some[T] if the next object is available, None if not, and the iterator advances each time Some is returned.
Is there a name for this pattern? I ask because I haven't really seen it before, and don't know what search terms to use either. Are there any libraries for it? (Composing Pending types and PendingIterators etc)
Is this a good way to implement a "concurrent" program anyway?
You basically have made a different version of Future. There is no more intrinsic “asyncness” to Future than there is to your trait. The only difference is that Future has a means to efficiently do nothing if there is currently nothing for it to do, by way of the Waker, but that's in a sense an optimization and not a fundamental difference. It's valid for the owner of a Future to poll() it any time it likes, just usually inefficient.
Is there a name for this pattern?
Ignoring comparisons to Future in particular, I would keep it simple and say that you have a form of cooperative multitasking here. Your long-running operation explicitly chooses to pause its work and relinquish the CPU to other concurrent tasks — that's exactly what cooperative multitasking is.
If you mean the pattern of returning Option for "nothing available yet", I don't know of a name for it, but one place it comes up a lot is channels, which often have a try_recv() method that returns an Option (or Result) containing a message only if there is a message, and that's exactly like your PendingIterator.
Personally, I would suggest having your function return Poll instead of Option — there is no functional difference, but the naming makes it clearer what your return means and how to use it. As I said, your Pending is pretty much exactly a Future minus the Waker mechanism, so bringing it closer, as far as that doesn't overcomplicate things, is good for familiarity.
Thanks for the replies. After thinking about it a bit more, I think that both Futures and this Pending interface are doing the same thing, they are both doing cooperative multitasking, and they essentially result in the same program. The choice between them is mostly up to taste. (Feel free to correct me if I am missing something.) With futures, calls to Future.await are the points where the current computation can give up control, while with Pending, it’s Pending.get. With Future, the code looks imperative, but it needs to be run in an async runtime. With Pending, no runtime is needed, but to do anything with a Pending value you’d probably use map or flatMap
Probably because of my Scala background, I thought of the functional approach first. But it seems like the Rust community has generally settled on Futures/await/async, and all the support is in that direction, so that’s probably the way to go for Rust programs.
That's pretty much the same in Rust, where async/ .await is syntactic sugar over Futures. You can hand-implement Futures [1], but it's often painful (if the state has self-references, you need to deal with Pinning, which is hard to wrap your head around).
These sort of functional combinators are available in Rust as well. They are provided by the FutureExt extension trait in the futures crate (flatMap is called then there). They aren't in the standard library because things get added there slowly and only as needed, due to the strong stability guarantees the standard library must uphold (Future only got put in the standard library when it was needed to support async/await).
.await is not part of Future, it is part of async/await syntax. The method on Future is Future::poll, and as I already said, it's nearly identical to yours. You could in fact replace your Pending trait with Future and get identical results.
Again, this confuses Future with async. “The code looks imperative” is a property of async blocks, not of Future. You can write Futures that are not made from async, but are just plain trait implementations. You can use map() and then() if you want to build futures functionally.
You need an async runtime to run arbitrary futures efficiently, but for futures which work like your Pendings that never want to sleep, it is sufficient to just call them the way you already are, providing a noop Waker in the context parameter. I'm not saying you should do that (though I would in your place), but that the difference with what you are doing is nearly zero.
Also, an async runtime can be very small, and 99% of the remaining complexity comes from supporting letting the future sleep and then be woken from an arbitrary thread.