How to talk about lifetime of opaque FnOnce result?

The following works with nightly but seems impossible to achieve on stable (the intention is to allow an async function foo that is passed as an argument to an async function baz to take a reference generated within the baz function):

type R<'a> = impl Future<Output = String> + 'a;

async fn baz<F>(text: String, f: F) -> String
where
    for<'a> F: FnOnce(&'a str) -> R<'a>,
{
    let result = f(&*text).await;
    result
}

Solving this on stable requires Pin<Box<dyn Future<...> + 'a>> because the lifetime 'a otherwise cannot be used to constrain the opaque return type of the FnOnce. Is there a fundamental reason behind this restriction?

This is better asked on users.rust-lang.org

The only way to solve this that I'm aware of it to create a new trait: Rust Playground

This trait can directly talk about lifetimes, and just forwards to calling the closure

pub trait BazInput<'a> {
    type Output: Future<Output = String>;
    
    fn get(self, s: &'a str) -> Self::Output;
}

impl<'a, F: FnOnce(&'a str) -> Fut, Fut: Future<Output = String>> BazInput<'a> for F {
    type Output = Fut;
    
    fn get(self, s: &'a str) -> Self::Output {
        self(s)
    }
}

Then baz just needs to use the trait, and specify it's one lifetime

async fn baz<F>(text: String, f: F) -> String
where
    for<'a> F: BazInput<'a>,
{
    f.get(&*text).await
}

And this can be then be used with async functions:

async fn foo(text: &str) -> String {
    text.to_string()
}

async fn test() -> String {
    baz("".to_string(), foo).await
}

Not possible now, but maybe in the future once associated type bounds becomes more general, we can do something like this: Rust Playground

#![feature(associated_type_bounds)]
#![feature(unboxed_closures)]

use core::future::Future;

async fn baz<F>(text: String, f: F) -> String
where
    for<'a> F: FnOnce<(&'a str,), Output: Future<Output = String>>,
{
    f(&*text).await
}

async fn foo(text: &str) -> String {
    text.to_string()
}

async fn test() -> String {
    baz("".to_string(), foo).await
}

You don't actually need a trait talking about lifetimes directly, or a trait with any methods. The trait can instead just treat the argument whole function argument generically. And the right supertraits allow you to use still use proper closure call syntax and not need any new methods.

For some code examples see this post on URLO

1 Like

Indeed, there are more workarounds as you and @steffahn show. This feels like a confirmation for my suspicion that the desirable function signature is indeed sound, but it is prevented by syntax restrictions, which is why I asked here about it.

In other words, why is there no way to write this signature down with a single where clause?

where for<'a> F: FnOnce(&'a str) -> R, R: Future<Output = String> + 'a

felt natural to me, but the 'a lifetime cannot be used in the definition of R. Another attempt was

where for<'a> F: FnOnce(&'a str) -> (R + 'a), R: Future<Output = String>

which is rejected on the grounds that R + 'a is a trait object — which can be made to work by boxing, of course, but again it is not obvious in terms of language design why the addition of a lifetime to this type constraint is interpreted as a trait object.

Due to my engineering experience I expect that there are reasons behind these choices, and I’d like to know them :slight_smile: It feels like there are some aspects of opaque types that cause these wrinkles in the user experience: we need to talk about some parts of a thing that we keep deliberately hidden.

I don't think that there is a good reason for this restriction beyond it's uncommon. So people don't ask about it. So no one's fixing it. This problem only manifests if you are using HRTB, which is already uncommon. Then you are mixing in an associated type that depends on the HRTB lifetime.

However, I think that associated type bounds could be extended to support this, without extra helper traits.

Good to know! I think this is becoming far less uncommon now that people are starting to routinely use async fn — this pops up for any higher-order async function that passes a reference to the inner function.

Since I’m not so familiar with associated type bounds, where would you recommend me to point a github issue at? Tracking issue for RFC 2289, "Associated type bounds" · Issue #52662 · rust-lang/rust · GitHub seems most relevant.

Yep, that's the one, looks like this is a known problem!