What is more idiomatic between passing a pin!ned Future or using pin_project on an Unpinned Future?

What are the reasons for preferring one over the other between the two bottom examples? Is one more idiomatic than the other?

pin! Future before passing it to another Future that polls the passed Future without any unsafe code:

use core::{
    future::Future,
    pin::{pin, Pin},
    task::{Context, Poll},
};
use tokio::runtime::Builder;
struct Foo<F> {
    x: F,
}
impl<F: Future + Unpin> Future for Foo<F> {
    type Output = F::Output;
    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        Pin::new(&mut self.x).poll(cx)
    }
}
fn main() {
    Builder::new_multi_thread()
        .enable_all()
        .build()
        .unwrap()
        .block_on(async move {
            tokio::spawn(foo()).await;
        });
}
async fn foo() {
    Foo {
        x: pin!(<some potentially !Unpin Future>),
    }
    .await;
}

Pass Future into another Future that polls the passed Future by relying on pin projection via pin_project_lite:

use core::{
    future::Future,
    pin::Pin,
    task::{Context, Poll},
};
use pin_project_lite::pin_project;
use tokio::runtime::Builder;
pin_project! {
    struct Foo<F> {
        #[pin]
        x: F,
    }
}
impl<F: Future> Future for Foo<F> {
    type Output = F::Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        self.project().x.poll(cx)
    }
}
fn main() {
    Builder::new_multi_thread()
        .enable_all()
        .build()
        .unwrap()
        .block_on(async move {
            tokio::spawn(foo()).await;
        });
}
async fn foo() {
    Foo {
        x: <some potentially !Unpin Future>,
    }
    .await;
}

I am guessing the former is preferred when pin_project/pin_project_lite is not a dependency (direct or indirect) in your crate. I'm guessing the latter is preferred when Foo is going to be part of a public API since its Future implementation is more general. What about the situation when pin_project/pin_project_lite is already a dependency but Foo is not part of the public API?

1 Like

It could be the case that Foo is more complex and cannot use pin_project, in which case the choice would be between writing unsafe code and requiring Unpin. Other than that, you have already listed all of the considerations I know of.

Could also be the case that pin_project works but pin! does not (at least without more complex machinery). For example:

tokio::spawn(Foo { x: pin!(<some potentially !Unpin Future>), }).await won't compile requiring you to add "another indirection" by using an async block/fn.

For some reason, I find the pin_project approach more appealing because I really am trying to perform pin projection; and pin!ing a Future just to get around that fact seems more like a "hack"—I know it's not a hack, but it's the best way I can describe my "feeling" as subjective as that is. Of course pin_project can mean more code especially when you don't want pin projection for some fields. For example, if Foo contained a bool, I would have to explicitly dereference the "projected" bool field as opposed to not. Then again, I like using core when possible even if I already have a dependency that could do something similar.

If one way is not any more idiomatic than the other, then I'll just decide on my own "rule" and be consistent with it.

I don't think you should be looking for a consistent rule first here. Do what works, and change it when it doesn’t work, as long as it isn't public API.

I'm not looking for a rule first. When one approach works and the other doesn't, then that matters more. When both work, then I like picking one style and sticking with it. If one isn't more idiomatic than the other, then I can arbitrarily pick one and stay consistent. I don't view this differently than naming conventions or even my own grammar/linguistic rules in English. Often times all possibilities are equally "correct", but I like consistency so long as I don't project my own somewhat arbitrary rules onto others (i.e., mistaken my rules as the rules).