HRTB on multiple generics

I hit a case where I need to apply HRTB to multiple generics. I can write the code I want without generics, but I can't figure out how to make it generic.

(Thanks in advance if you decide to read through this whole thing! :slight_smile:)

Let me take you on a journey. First, let's look at some working code that does everything I need (but without generics):

fn print_func_result<F>(func: F)
where
    for<'a> F: FnOnce(&'a str) -> &'a str,
{
    let local_string = String::from("test");
    let result = func(&local_string);
    println!("{:?}", result);
}

fn accept_me(s: &str) -> &str { s }

fn reject_me(s: &'static str) -> &'static str { s }

fn main() {
    print_func_result(|x| accept_me(x));
    print_func_result(|x| reject_me(x));
}

The local_string var is owned by print_func_result, so Rust will reject reject_me(x) because the borrow would escape its lifetime. This is good. So far all is well in the world.

Now, let's try to make print_func_result generic:

fn print_func_result<D, F>(func: F)
where
    D: Debug,
    for<'a> F: FnOnce(&'a str) -> D,
{}

This will not compile, because (I believe) the compiler uses a default 'static bound for D. The correct bound for D is actually D: Debug + 'a, but you can't write that type because the name 'a is not in scope outside the HRTB.

Here are a few of my failed attempts:

fn print_func_result<D, F>(func: F)
where
    D: Debug,
    for<'a> F: FnOnce(&'a str) -> (D + 'a),
{}
fn print_func_result<D, F>(func: F)
where
    for<'a> (
        D: Debug + 'a,
        F: FnOnce(&'a str) -> D + 'a,
    )
{}

And here's a playground link if you'd like to share in my suffering.

I'm sure there's some magic incantation that will make this work. Can anyone enlighten me?

You can use a helper trait: playground. The idea is the same as your last code block, but to pack several constraints behind a for bound, you have to use a trait.

4 Likes

Note that using a closure won't work out that well because Rust is bad at inferring the most general bounds required for closures.

You can help Rust along with this,

print_func_result({
    fn enforce_bounds<F: FnOnce(&str) -> &str>(f: F) -> F { f }
    enforce_bounds(|x| accept_me(x))
});

But this is rather ugly. If you can, try and avoid these higher rank bounds. Especially when dealing with closures.

2 Likes

Yeah I ran in to that too, which is why you'll find me passing just the function instead of a closure.

1 Like

Yeah, I was just about to post that solution when I saw your answer. Beat me to it! I was trying to see if I could find a better solution to this than an ugly function call to enforce the correct bounds, but no such luck.

Thanks for taking a look!

I tried it out in my real app, and unfortunately the example was a little too simplified. accept_me and reject_me actually return existential types, not concrete types:

fn accept_me(s: &str) -> impl Debug + '_ { s }

fn reject_me(s: &'static str) -> impl Debug + 'static { s }

If I try to write enforce_bounds for the above, I end up with this:

fn enforce_bounds<D: Debug, F: FnOnce(&str) -> D>(f: F) -> F { f }

And now I need to add the lifetime to D. It seems like I'm right back where I started. Here's another playground link.

If this is a limitation of inference, it seems like I hit a wall :confused:. Any ideas?

You can still use the function name directly:

print_func_result(accept_me);

But I'm not sure if you can convince it to work with closures.

In my real code, not even using the function directly works. But even if it did, this is getting pretty gnarly, I think I'll heed @KrishnaSannasi's advice and try to rearchitect to avoid this situation.

Thanks for helping me out!

2 Likes