Make my function to be able to accept "any" closure

In Rust, I can make my function to accept a closure that is required to return an instance of some trait, say, Debug:

fn f(p: impl Fn(&str) -> Box<dyn Debug>) {}

Such function seems to work:

f(|b| Box::new("static")); //< works

However, it can't accept a closure that returns a value borrowed from the closure's argument:

f(|b| Box::new(b)); //< doesn't work

Neither it can accept a closure that returns a value borrowed from the outer context:

let a = &"a".to_owned();
// f(|b| Box::new(a)); //< doesn't work

I know how to make my function to be able to accept a closure that returns a value borrowed from the closure's argument:

fn f(p: impl for<'t> Fn(&'t str) -> Box<dyn 't + Debug>) {}
f(|b| Box::new("static")); //< works
f(|b| Box::new(b)); //< also works

However, such function still can't accept a closure that returns a value borrowed from the outer context.

Also, I know how to make my function to be able to accept a closure that returns a value borrowed from the outer context:

fn f<'t>(p: impl 't + Fn(&str) -> Box<dyn 't + Debug>) {}
f(|b| Box::new("static")); //< works
let a = &"a".to_owned();
f(|b| Box::new(a)); //< also works

However, such function can't accept a closure that returns a value borrowed from the closure's argument.

How can I write a function that is able to accept a closure that returns a value borrowed from the closure's argument, and is able to accept a closure that returns a value borrowed from the outer context, and is able to accept a closure that returns a value borrowed from both? E.g.:

fn f<???>(p: ???) {}
f(|b| Box::new("static")); //< must work
f(|b| Box::new(b)); //< must work too
let a = &"a".to_owned();
f(|b| Box::new(a)); //< must work too
f(|b| Box::new((a, b))); //< must work too
1 Like

I don't think you can have EXACTLY this in Rust, that's because there is a fundamental lifetime conflict here O.O
But is there a practical case where this would be needed? Or is this just a theoretical curiosity?

But the examples you provided in the last block should pass with the following signature... I think, if lifetime is not an issue.

fn f<'a, F>(p: F) 
where 
    F: Fn(&'a str) -> Box<dyn Debug + 'a> 
{
}
1 Like

Something like this works for me ...

    fn f<'t>(p: impl 't + for<'a> Fn(&'a str, &'a &'t ()) -> Box<dyn Debug + 'a>) {
        // just an example
        let hi = "hi".to_owned();
        println!("{:?}", p("hello", &&()));
        println!("{:?}", p(&hi, &&()));
    }
    f(|b, _| Box::new("static")); //< must work
    f(|b, _| Box::new(b)); //< must work too
    let a = &"a".to_owned();
    f(|b, _| Box::new(a)); //< must work too
    f(|b, _| Box::new((a, b))); //< must work too

It uses an extra "fake" parameter to enforce the lifetime bound.

5 Likes

Practical case.

I'm writing a tool that generates some C and C++ code (it takes a C++ code of specific kind as input and generates some wrapping/boilerplate code in C and C++). I have a plenty of structs for various kinds of declarations (like StructDeclaration, FunctionsDeclaration, etc), expressions (AggregateInitialization, ListInitialization, FunctionCall, etc) and etc, which all implement my custom trait Formattable. Formattable is like std::fmt::Display but it allows to generate code with a proper formatting (line wrapping, indentation, alignment) for a specific context. I.e. I build a tree of Formattables that contains child Formattables that contains grandchild Formattables and so on, and then I call Formattable::format on the top-level Formattable and it generates a properly formatted String.

Now I want to create a, say, generate_foo function that generates some stuff and during that process it needs to wrap certain expressions (let's call them "bar expressions") using a strategy that is specified as a generate_foo's parameter. At first approximation, it's like:

fn generate_foo(…, wrap_bar_expression: impl Fn(String) -> String, …) -> String;

where I can pass |v|format!("*{v}") or |v|format!("{v} + 1") or |v|format!("f({v})") or etc as wrap_bar_expression.

Except that I deal not with Strings but with Formattables. So it's like

fn generate_foo(
    …,
    wrap_bar_expression: impl Fn(Box<dyn Formattable>) -> Box<dyn Formattable>,
    …
) -> Box<dyn Formattable>;

And these Formattables are not static, of course:

fn generate_foo(
    …,
    wrap_bar_expression: impl for<'a> Fn(Box<dyn 'a + Formattable>) -> Box<dyn 'a + Formattable>,
    …
) -> Box<dyn Formattable>;

But then I have a case where a wrap_bar_expression closure uses an outer Formattable. Like let wrap_bar_expression = |v|format!("f({v}, {mode})");, except that (again) I deal not with Strings but with Formattables, i.e. v, mode and result are all Box<'? + Formattable>s, and the result is a tree that then owns v and mode as its nodes.

1 Like

I assume you are trying to avoid cloning here, which leads you to use HRTB's? Otherwise using a wrapper trait with a clone bound would be pretty straightforward.

1 Like

"Not static" is probably not specific enough; I think you need to consider what they borrow from. In case it's e.g. an input of the generation process, you could change the signature to

fn generate_foo<'a>(
    …,
    wrap_bar_expression: impl Fn(Box<dyn 'a + Formattable>) -> Box<dyn 'a + Formattable>,
    …
) -> Box<dyn 'a + Formattable>;

so if the closure captures something with lifetime 'a it can include that in the returned value. Or rather, the borrow checker will choose 'a depending on what you return from the closure, among other things.

(Edit: Just noticed that this is basically what @consistent-milk12 wrote in his second answer.)

1 Like

isn't &'a &'b () unsound right now tho? if i remember correctly that's the main trick of cve.rs

consider moving the relevant data into the closure instead of using a reference to the outer context.
otherwise you are describing fundamentally different scenarios so it might be advisable you do not use the same implementation for them all

It needs a complicate construction to actually trigger the bug (which almost never happens accidentally). Let's not pretend any nested reference usage is unsound. Claiming that is basically equivalent to saying Rust is literally unusable.

3 Likes

The soundness bug you're referring to is based on higher-ranked binders (for<'a> ...) not being conditional (some hypothetical for<'a where 'b: 'a>) when they should be (after particular subtype coercions).

That particular unsoundness doesn't arise in natural code. Don't get tricked into thinking it is likely, much less common, due to a meme crate.[1]

@Tom47's signature, with the nested lifetime in a bound, is a way to emulate conditional binders on stable Rust.[2] It effectively says "your closure must work with all lifetimes shorter than some cap of your choosing". The conditional -- the cap -- is what makes it compatible with closures that capture and return borrows of locals (as desired by the OP).

Scoped threads use a similar mechanism to be ergonomic and sound.


  1. See also some recent discussion here. ↩︎

  2. (A function pointer to f wouldn't even be like the ones discussed in the soundness bug, as it has no late-bound parameters itself.) ↩︎

1 Like

Here is a simplified version where you won't interact with references and lifetimes.

fn f(p: impl FnOnce(String) -> Box<dyn Debug>) { // NOTE: use FnOnce because one String can only be moved once
    let s: String = "s_created_in_f()".to_owned();
    println!("{:?}", p(s));
}

fn main() {
    f(|b: String| Box::new("static")); //< must work
    f(|b: String| Box::new(b)); //< must work too
    let a: String = "a".to_owned(); // NOTE: we need an owned String object, not a reference (`&` removed)
    f(|b: String| Box::new(a)); //< must work too
    let a: String = "a".to_owned(); // NOTE: create a new String because the previous one was moved into the closure above
    f(|b: String| Box::new((a, b))); //< must work too
}
# output:
"static"
"s_created_in_f()"
"a"
("a", "s_created_in_f()")

From what I read a few days ago from The Book, every closure implements the FnOnce trait, those wich don't take ownership of captured values also implement the FnMut trait and those who don't borrow captured variables mutably also implement the Fn trait.
This results in functions implementing all of the traits because they work in all instances but the closures that move captured variables can only implement FnOnce so to make it work for every closure, you would need to make it work for FnOnce.