-
-
Notifications
You must be signed in to change notification settings - Fork 39
Description
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