user457586
user457586

Reputation:

Using iter.map, why does a closure work but passing the function directly does not?

I've got this function in Rust that capitalizes a string.

pub fn capitalize_first(input: &str) -> String {
    let mut c = input.chars();
    match c.next() {
        None => String::new(),
        Some(first) => first.to_uppercase().collect::<String>() + c.as_str(),
    }
}

Later, I use it to iterate over a vector of strings.

let words = vec!["hello", "world"];
let capitalized_words: Vec<String> =
    words.iter().map(|word| capitalize_first(word)).collect();

This works as expected, but I notice that the closure |word| capitalize_first(word) is pretty useless. So I tried to replace it with passing capitalize_first directly like this.

let words = vec!["hello", "world"];
let capitalized_words: Vec<String> = words.iter().map(capitalize_first).collect();

This, however, fails to compile with the following error message.

10 | pub fn capitalize_first(input: &str) -> String {
   | ---------------------------------------------- found signature of `for<'r> fn(&'r str) -> _`
...
38 |         let capitalized_words: Vec<String> = words.iter().map(capitalize_first).collect();
   |                                                               ^^^^^^^^^^^^^^^^ expected signature of `fn(&&str) -> _`

I'm having trouble understanding this error. Why does the closure work but passing the function directly does not. Is there something I can change that would allow me to pass the function reference instead of making a useless closure?

Upvotes: 5

Views: 2983

Answers (1)

Benjamin Lindley
Benjamin Lindley

Reputation: 103713

When you iterate over a collection like you are with your call to words.iter(), you are iterating over references to your elements. The elements in your vector are of type &str, and so references to your elements are of type &&str.

So map is expecting a function which takes an argument of type &&str, and so that is what the word argument in your closure is inferred as. Then when you call capitalize_first on word, it is automatically dereferenced to &str, due to the Deref trait being implemented for all references.

However, even though your function will appear to accept arguments of type &&str due to this conversion, the conversion happens outside your function. So it doesn't mean that your function can be passed in place of a function which expects &&str.

There are 2 solutions. You can either change your function to be more generic, and have it accept anything that implements AsRef<str>. Then it will accept anything that can be dereferenced to str, which includes &str, &&str and String, among others.

pub fn capitalize_first<S: AsRef<str>>(input: S) -> String {
    let mut c = input.as_ref().chars();
    // Don't forget this ^

    match c.next() {
        None => String::new(),
        Some(first) => first.to_uppercase().collect::<String>() + c.as_str(),
    }
}

Or you can keep your function as it is, and fix it at the call site, by calling copied() on your iterator, which will essentially dereference the elements.

let capitalized_words: Vec<String> = words.iter().copied().map(capitalize_first).collect();

I would recommend the first approach.

Upvotes: 7

Related Questions