Skip to content

Make conditional project_replace on enums more ergonomic ? #360

@Morgane55440

Description

@Morgane55440

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions