Skip to content

← Async From First Principles step 13 of 25

Easy Primitives

`poll_fn`: a future from a closure

After several hand-written impl Future blocks you have earned the shortcut. std::future::poll_fn (stable 1.64) turns a closure of the right shape straight into a future:

pub fn poll_fn<T, F>(f: F) -> PollFn<F>
where F: FnMut(&mut Context<'_>) -> Poll<T>

That signature is worth staring at. A future is nothing but a thing with a poll method, so anything with the same shape as poll can be one.

pub fn yield_n(n: u32, log: Rc<RefCell<Vec<u32>>>) -> impl Future<Output = u32>
pub fn drive_all(script: Vec<u32>) -> (Vec<u32>, Vec<u32>)

yield_n(n, log) must, on every poll, append the 1-based number of that poll to log; return Pending for the first n polls, asking to be polled again each time; and resolve to the total number of polls it received.

drive_all shares one log across the whole script, drives one yield_n(n, log) per entry in order with the supplied block_on, and returns (log, results). For script = [2, 0, 1] that is log = [1, 2, 3, 1, 1, 2] and results = [3, 1, 2].

Where the state lives

The closure you hand to poll_fn is FnMut, so it can own and mutate captured variables — a counter, a buffer, a handle. Those captures are stored inside the returned future, and they are pinned along with it. That means poll_fn state can be self-referential in principle; do not go looking for that today, item 16.10 is where it belongs.

The practical point is simpler. This is the standard way to bridge a poll-style API into async: you have something with a try_recv-shaped function and you need a future, so you wrap it.

The lint you are here to read

The starter returns impl Future<Output = u32> with an async block inside:

pub fn yield_n(..) -> impl Future<Output = u32> {
    async move { .. }
}

clippy calls that clippy::manual_async_fn — “this function can be simplified using the async fn syntax” — and since the gate is -D warnings, it is fatal. The lint is right about the shape it sees: a plain function whose whole body is one async block really is just an async fn written the long way.

But notice what it does not fire on. Returning impl Future is perfectly fine when the body is a poll_fn call, a std::future::ready, a combinator, or anything else that is not a bare async block. The lint targets the redundant wrapper, not the return type. Knowing that distinction means the message is legible instead of alarming the next time you meet it.

Why an async fn cannot cheat its way out of this

Notice that the log records polls, not iterations. An async block cannot observe its own polls; only a type that implements Future can. So the shortest honest route to this specification is poll_fn with a captured counter — which is exactly the point.

Two neighbours worth knowing

std::future::ready(v) is a future that is Ready on its first poll — the Immediate you wrote in item 16.3, already in std.

std::future::pending::<T>() never completes. Handing it to a block_on that busy-spins hangs your program forever; it is only useful inside a race, or under a driver with a poll budget like the one supplied here.