How to be generic over fn(&str) and fn(String)

fn check<F, T>(f: F)
where
    F: Fn(T),
    T: for<'a> From<&'a str>,
{
    let my_str = String::from("hello");
    (f)(my_str.as_str().into())
}

fn main() {
    fn fn_1(_: &str) {}
    fn fn_2(_: String) {}
    
    check(fn_1);
    check(fn_2);
}

(Playground)

The above code reasonably fails because there is no 'b such that &'b str: for<'a> From<&'a str> as I understand it. But is it possible to achieve this with the following constraints?

  • use From trait.
  • the string passed is owned by check() stack.

If we are allowed to use a different trait, what would it be?

1 Like

The problem is that Fn(&str) is a shorthand for the HRTB ("higher-ranked trait bound") for<'a> Fn(&'a str), which is not a special case of Fn(T). Since check intends to pass a borrow of a local variable to F in the Fn(&str) case, it's not possible to instead expect Fn(&'a str) for any concrete lifetime 'a, since local variables cannot be borrowed for the duration of an external lifetime parameter.

The best approach might be to introduce your own trait here and put it as a trait bound on F. Conceptually, something like

trait StringFunction {
    fn call(&self, s: String);
}

fn check<F>(f: F)
where
    F: StringFunction
{
    let my_str = String::from("hello");
    f.call(my_str);
}

seems desirable. You could then implement it as follows.

impl StringFunction for fn(String) {
    fn call(&self, s: String) {
        self(s)
    }
}

impl StringFunction for fn(&str) {
    fn call(&self, s: String) {
        self(&s)
    }
}

fn main() {
    fn fn_1(_: &str) {}
    fn fn_2(_: String) {}
    
    check(fn_1 as fn(&str));
    check(fn_2 as fn(String));
}

However this only works with function pointers and you'd need to cast functions to function pointers (and accept potential runtime-overhead).

If you want things to work with all Fn-implementing types including function items (and closures ) this no longer works; we cannot generically implement this trait for all Fn-implementors, since (at least in theory / with unstable Rust features) there could be overlapping implementations; i.e. a type F could be both Fn(String) and Fn(&str) at the same time. In this case, introducing a dummy parameter and relying on type inference may help:

trait StringFunction<T> {
    fn call(&self, s: String);
}

fn check<F, T>(f: F)
where
    F: StringFunction<T>
{
    let my_str = String::from("hello");
    f.call(my_str);
}

impl<F> StringFunction<String> for F where F: Fn(String) {
    fn call(&self, s: String) {
        self(s)
    }
}

impl<F> StringFunction<&str> for F where F: Fn(&str) {
    fn call(&self, s: String) {
        self(&s)
    }
}

fn main() {
    fn fn_1(_: &str) {}
    fn fn_2(_: String) {}
    
    check(fn_1);
    check(fn_2);
}
5 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.