pin-project icon indicating copy to clipboard operation
pin-project copied to clipboard

Make conditional `project_replace` on enums more ergonomic ?

Open Morgane55440 opened this issue 6 months ago • 6 comments

this seems like a big ask, and my solution introduces a lot of types, but it would help making pin projection and pin_replace much simpler in user code.

here is a simple enum based future with 2 :

#[pin_project(project = ProjFut, project_replace = ProjFutOwn)]
enum CutomFuture {
    Innit { input1: T, input2: T, },
    Await1 { local1: U, #[pin] fut1: SomeFut, input2: T, },
    Await2 { local1: U, #[pin] fut2: SomeFut, },
    Complete,
}

impl Future for CutomFuture {
    type Output = O;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> std::task::Poll<Self::Output> {
        if matches!(self.as_mut().project(), ProjFut::Innit { .. }) {
            let ProjFutOwn::Innit {
                input1,
                input2,
            } = self.as_mut().project_replace(Self::Complete)
            else {
                unreachable!("we just checked")
            };
            
            // code before the awaits

            Pin::set(
                &mut self,
                CutomFuture::Await1 {
                    local1,
                    fut1,
                    input2
                },
            );
        }
        if let ProjFut::Await1 { fut1, .. } = self.as_mut().project() {
            let res = match fut1.poll(cx) {
                Poll::Ready(res) => res,
                Poll::Pending => return Poll::Pending,
            };
            let ProjFutOwn::Await1 {
                local1, input2, ..
            } = self.as_mut().project_replace(Self::Complete)
            else {
                unreachable!("we just checked")
            };
            
            // code between the awaits

            Pin::set(&mut self, CutomFuture::Await2 { local1, fut2 });
        }
        if let ProjFut::Await2 { fut2, .. } = self.as_mut().project() {
            let res = match fut2.poll(cx) {
                Poll::Ready(res) => res,
                Poll::Pending => return Poll::Pending,
            };
            let ProjFutOwn::Await2 { local1, .. } = self.as_mut().project_replace(Self::Complete)
            else {
                unreachable!("we just checked")
            };

            // code after the awaits

            return Poll::Ready(output);
        }
        Poll::Pending
    }
}

i want to point out that it really amazing that this can be done fully from safe code, basically as optimized as possible ( ignoring self referential futures of course) .

that being said the unreachable bits are kinda painful.

thus my idea : introduce 2n + 1 new types per enum with n variants for project, and another n for project replace, as follows : KnownVariantName<'pin>(Pin<&'pin mut Enum>) is a thin wrapper that guarantees the variant it holds. it can be mutably borrowed through .project() to make. project reborrows here by default, so project_into could take by value to preserve lifetimes. KnownVariantNameProj<'pin>{ ..projected fields of the variant } which is identical to the regular proj on enum except it's a type and not an enum variant. of course to get a KnownVariantName, you use something like .variant() on the pinned Enum, returning a EnumVariant() enum. that acounts for 2n + 1 types, and doesn't provide anything useful.

but where this becomes actually useful is the KnownVariantNameProjOwn( ... non pinned fields). this type can be obtained simbly through calling project_replace on KnownVariantName which takes it by value, and takes an Enum is input, meaning after project_replace, the value inside the Enum is statically indeterminate again.

meaning the above code wold become :

#[pin_project(project = ProjFut, project_replace = ProjFutOwn, know_variants)]
enum CutomFuture {
    Innit { input1: T, input2: T, },
    Await1 { local1: U, #[pin] fut1: SomeFut, input2: T, },
    Await2 { local1: U, #[pin] fut2: SomeFut, },
    Complete,
}

impl Future for CutomFuture {
    type Output = O;

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> std::task::Poll<Self::Output> {
        if let CutomFutureVariant::Innit(innit) {

            let KnownInnitProjOwn {
                input1,
                input2,
            } = innit.project_replace(Self::Complete);
            
            // code before the awaits

            Pin::set(
                &mut self,
                CutomFuture::Await1 {
                    local1,
                    fut1,
                    input2
                },
            );
        }
        if let CutomFutureVariant::Await1(await1) {
            let KnownAwait1Proj { fut1, .. } = await1.project()
            let res = match fut1.poll(cx) {
                Poll::Ready(res) => res,
                Poll::Pending => return Poll::Pending,
            };
            let KnownAwait1ProjOwn  {
                local1, input2, ..
            } = await1.project_replace(Self::Complete);
            
            // code between the awaits

            Pin::set(&mut self, CutomFuture::Await2 { local1, fut2 });
        }
        if let CutomFutureVariant::Await2(await2) {
            let KnownAwait2Proj { fut2, .. } = await2.project()
            let res = match fut2.poll(cx) {
                Poll::Ready(res) => res,
                Poll::Pending => return Poll::Pending,
            };
            let KnownAwait2ProjOwn  {
                local1, ..
            } = await2.project_replace(Self::Complete);

            // code after the awaits

            return Poll::Ready(output);
        }
        Poll::Pending
    }
}

as i said, this is a lot of types, for something that can be done safely through unreacheables, and maybe pattern types could solve this in the future when they are eventually implemented, but i figured it wouldn't hurt to ask.

if you feel this could fit the crate, i am very willing to help fix any design issues and implementing this

Morgane55440 avatar Jun 26 '25 18:06 Morgane55440