Why does `usize` works here but not `&'static str`?

Apparently, &'static strs are just as well-behaved as integers (they're Copy, Send, Sync, Sized, and 'static). So, what's wrong in the case below?

#![allow(unused)]
use futures::{StreamExt, TryStreamExt, stream};

const ID: [&str; 2] = ["foo", "bar"];

struct Library;

impl Library {
    async fn update(&self) -> Result<(), ()> {
        // compiles
        #[cfg(true)]
        stream::iter([0, 1])
            .map(|idx| async move {
                match ID[idx] {
                    "foo" => Ok(()),
                    "bar" => Ok(()),
                    _ => unreachable!(),
                }
            })
            .buffer_unordered(2)
            .try_collect::<()>()
            .await?;

        // error
        #[cfg(false)]
        stream::iter(ID)
            .map(|id| async move {
                match id {
                    "foo" => Ok(()),
                    "bar" => Ok(()),
                    _ => unreachable!(),
                }
            })
            .buffer_unordered(2)
            .try_collect::<()>()
            .await?;

        Ok(())
    }
}

trait Execute {
    fn execute(self, lib: &Library) -> impl std::future::Future<Output = Result<(), ()>> + Send;
}

struct Update;

impl Execute for Update {
    async fn execute(self, lib: &Library) -> Result<(), ()> {
        lib.update().await?;
        Ok(())
    }
}

Dependencies: futures = "0.3.32".

What's the error?


My guess is that you're getting &&str in the iterator (if you used &ID), and the outer & is not static but borrowed temporarily. The fix for that is to dereference it or add .copied() to the iterator.

I can't explain it fully, but it has something to do with type inference for closures with higher ranked lifetimes.

although the type of the Stream::Item is a single type &'static str, for some reason when a reference type is involved (even if the reference has a lifetime of 'static), the type checker want the closure to have higher ranked lifetimes. if you replace the closure with a named function, it compiles:

async fn mapper(id: &str) -> Result<(), ()> {
    match id {
        "foo" => Ok(()),
        "bar" => Ok(()),
        _ => unreachable!(),
    }
}
// alternatively:
fn mapper(id: &str) -> impl Future<Output = Result<(), ()>> {
    async move {
        match id { ... }
    }
}

stream::iter(ID)
    .map(mapper)
    .buffer_unordered(2)
    .try_collect::<()>()
    .await?;

a usual workaround for similar inference error is to use an explicit annotated coersion site to wrap the closure, but unfortunately in this case, the return type of the closure is an unnamable future, it is impossible to annotate, unless you want to box the returned future of the closure:

fn funnel<F>(f: F) -> F
where
    F: FnMut(&str) -> Pin<Box<dyn Future<Output = Result<(), ()>> + Send + '_>>,
{
    f
}

stream::iter(ID)
    .map(funnel(|id| {
        async move {
            match id {
                "foo" => Ok(()),
                "bar" => Ok(()),
                _ => unreachable!(),
            }
        }
        .boxed()
    }))
    .buffer_unordered(2)
    .try_collect::<()>()
    .await?;

The inner closure must accept any lifetime

If the named function accepts a &'static str, it still fails.

It’s definitely something to do with the trait solver being weird about references as inputs… replace the input with some struct Foo(&’static str) (and add ID.map(Foo), id.0 where appropriate) so it shouldn’t be a trait bound issue.

I’m suspicious of some where Self: Sized bounds…. I recall those being inexplicably weird when higher-order closures are involved.

funny enough, if you create a wrapper type, then the type checker will not demand a closure with higher ranked trait bounds, because the wrapper type doesn't have a lifetime:

struct Id(&'static str);
const ID: [Id; 2] = [Id("foo"), Id("bar")];

stream::iter(ID)
    .map(|id| async move {
        match id.0 {
           "foo" => Ok(()),
           "bar" => Ok(()),
            _ => unreachable!(),
        }
     })
//...

interesting, I just read through the linked issue where it is mentioned that Send actually triggers the error. if I remove the Send bound in the original example code, it indeed compiles:

 trait Execute {
-    fn execute(self, lib: &Library) -> impl std::future::Future<Output = Result<(), ()>> + Send;
+    fn execute(self, lib: &Library) -> impl std::future::Future<Output = Result<(), ()>>;
 }

so the "not generic enough" is not the real error, it's just the compiler failed to report the error accurately.