How do I pass a method struct as a function

I want to pass a method from a struct as a function. I am unsure if I need to change a function signature somewhere or add lifetime parameters- I have tried both but am struggling. Confession: when passing functions as arguments, I don't fully have my head around the difference between Fn() and &dyn Fn() and &impl Fn().

Here is a playground link.

Here is the code:

use std::vec::Vec;

struct Document {
    words: Vec<String>,
}

impl Document {
    // if a word in the document is 'recognized' by the highlighter function, mark it with HTML bold brackets
    fn highlight_me(&mut self, highlighter: &dyn Fn(&str)->bool) {
        for word in &mut self.words {
            if highlighter(&word) {
                *word = format!("<b>{}</b>", word)
            }
        }
    }
}

struct Glossary {
    // a list of words you want to highlight if you find them
    keywords: Vec<String>
}

impl Glossary {
    fn highlight_doc(&self, word: &str) -> bool {
        for kw in &self.keywords {
            if kw == word {
                return true
            }
        }
        false
    }
}


fn main() {
    let mut doc = Document{words: vec!["How".to_string(), "Foo".to_string(), "works".to_string()]};
    let glossary = Glossary{keywords: vec!["Foo".to_string()]};
    let closure = |word| glossary.highlight_doc(word);
    doc.highlight_me(&closure)
}

And here is the error: again I have tried specifying lifetimes here and there but I a missing something:

39 |     doc.highlight_me(&closure)
   |                      ^^^^^^^^ implementation of `FnOnce` is not general enough
   |
   = note: closure with signature `fn(&'2 str) -> bool` must implement `FnOnce<(&'1 str,)>`, for any lifetime `'1`...
   = note: ...but it actually implements `FnOnce<(&'2 str,)>`, for some specific lifetime `'2`

1 Like

This is a classic case of Rust erroneously inferring a single lifetime for a closure parameter, when it should have inferred that the parameter is generic over any lifetime ("higher-ranked"). Here's a fix:

-    let closure = |word| glossary.highlight_doc(word);
+    let closure = |word: &_| glossary.highlight_doc(word);

In short, it's a weakness of the current compiler, not anything you did wrong.

3 Likes

Moving the closure into the function call also works (though taking a reference to a closure looks... weird)

doc.highlight_me(&|word| glossary.highlight_doc(word))

As for the difference between Fn() and &dyn Fn() and &impl Fn(), that has to do with how Rust actually generates code for the closure.

You could change highlight_me to

fn highlight_me<F: Fn(&str) -> bool>(&mut self, highlighter: &F)

And then a new version of highlight_me would be generated for every different closure passed to it.

impl Fn(&str) -> bool works the same as using the type parameter F in the previous example, but doesn't require naming the type parameter (impl Trait is a little different when it's a return type, but that's not relevant in this case).

The way you have it currently uses a trait object dyn Fn(&str) -> bool which means only a single version of highlight_me gets generated, and instead the closure reference gets passed with some extra metadata so highlight_me can figure out how to call whatever value the highlighter reference actually points to as as a Fn.

2 Likes

Thank you for the quick reply. There is some degree of comfort knowing that, for the first (and perhaps last) time in my programming efforts, the compiler actually is partially to blame :stuck_out_tongue:

Thank you! Your explanation is concise and helpful.