I am trying to design a future adapter, which would take Fn() -> Future<Output=Result>
and retry it.
Simple way to do it is plain function, which consumes future generator and produces a future:
impl<FF,F1,F2,R,E1,E2> WithRetry<F2> for FF
where
FF: Fn() -> F1,
FF::Output: Future<Output=Result<R,E1>>,
F1: Future<Output=Result<R,E1>>,
F2: Future<Output=Result<R,E2>>,
E1: ShouldRetry + std::fmt::Debug,
{
fn with_retry(self, delay: Duration) -> Retry<F2> {
let f = async {
loop {
match self().await {
Ok(res) => return Ok(res),
Err(e) => match e.should_retry() {
true => {
debug!("Error, will retry: {:?}", e);
tokio::time::sleep(delay).await;
continue;
}
false => return Err(e),
}
}
}
};
Retry(f)
}
}
Then calling the function in the code, like this:
with_retry(Duration::from_secs(1), (|| async {
...
Ok(result)
}))
Note: the problem I am trying to solve has more to do with education than practical limitations, so I am just trying to understand WHY and not so much HOW.
For ergonomic considerations I want it to be like this:
(|| async {
...
Ok(res)
}).with_retry(Duration::from_secs(1))
The design of this API is called "adapter" can be seen in many places in Rust and consists of introducing a struct, a trait and implementation of trait which links desired type (future generator) to the struct. Then you implement Future
for the struct as an adapter. But I am having problem with implementing link between my trait and structure:
pub struct Retry<FF>(FF);
pub trait WithRetry<F> {
fn with_retry(self, delay: Duration) -> Retry<F>;
}
impl<FF,F1,F2,R,E1,E2> WithRetry<F2> for FF
where
FF: Fn() -> F1,
FF::Output: Future<Output=Result<R,E1>>,
F1: Future<Output=Result<R,E1>>,
F2: Future<Output=Result<R,E2>>,
E1: ShouldRetry + std::fmt::Debug,
{
fn with_retry(self, delay: Duration) -> Retry<F2> {
let f = async {
todo!()
};
Retry(f)
}
}
error[E0308]: mismatched types
--> src/retry_policy.rs:73:15
|
48 | impl<FF,F1,F2,R,E1,E2> WithRetry<F2> for FF
| -- this type parameter
...
57 | let f = async {
| _______________________-
58 | | loop {
59 | | match self().await {
60 | | Ok(res) => return Ok(res),
... |
70 | | }
71 | | };
| |_________- the found `async` block
72 |
73 | Retry(f)
| ^ expected type parameter `F2`, found opaque type
|
::: /home/vadym/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/future/mod.rs:61:43
|
61 | pub const fn from_generator<T>(gen: T) -> impl Future<Output = T::Return>
| ------------------------------- the found opaque type
|
= note: expected type parameter `F2`
found opaque type `impl futures::Future`
= help: type parameters must be constrained to match other types
= note: for more information, visit https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters
While trying to solve this, I have read rfc-1522
"https://github.com/rust-lang/rfcs/blob/master/text/1522-conservative-impl-trait.md"
However, there is an issue with this, namely that in combinations with generic trait methods, they are effectively equivalent to higher kinded types. Which is an issue because Rust's HKT story is not yet figured out, so any "accidental implementation" might cause unintended fallout.
I start suspecting that I am hitting this exactly limitation. My intuition is as follow: When Rust generics are evaluated, today's rustc requires all generic type to be defined and resolved to concrete types (no traits) by the type caller. Now, because of async
, I use and return an anonymous type, and even more, this type is not a concrete but "something that implements Future". Today it is only possible to define "something that implements Trait" via type params and factual type of params must be defined by the caller. Which spells doom on my attempt to build such API because I am trying to build a concrete type in implementation.
Am I right that I am hitting HKT limitations of today's rust?
P.S. Perhaps another way to still achieve desired API would be to use Box<dyn Future<Output=Result<R,E>>
? This way no concrete type is generated inside my implementation and R and E are supplied by the type caller?