Search code examples
rustiteratordereferencetype-coercion

What's the difference between `.map(f)` and `.map(|x| f(x))`?


When doing rustlings standard_library_types/iterators2.rs, I started wondering how std::iter::Iterator::map calls its argument closure/function. More specifically, suppose I have a function

// "hello" -> "Hello"
pub fn capitalize_first(input: &str) -> String {
    let mut c = input.chars();
    match c.next() {
        None => String::new(),
        Some(first) => String::from(first.to_ascii_uppercase()) + c.as_str(),
    }
}

Now I want to use it in

// Apply the `capitalize_first` function to a slice of string slices.
// Return a vector of strings.
// ["hello", "world"] -> ["Hello", "World"]
pub fn capitalize_words_vector(words: &[&str]) -> Vec<String> {
    words.into_iter().map(capitalize_first).collect()
}

which doesn't compile

error[E0631]: type mismatch in function arguments
  --> exercises/standard_library_types/iterators2.rs:24:27
   |
11 | pub fn capitalize_first(input: &str) -> String {
   | ---------------------------------------------- found signature of `for<'r> fn(&'r str) -> _`
...
24 |     words.into_iter().map(capitalize_first).collect()
   |                           ^^^^^^^^^^^^^^^^ expected signature of `fn(&&str) -> _`

error[E0599]: the method `collect` exists for struct `Map<std::slice::Iter<'_, &str>, for<'r> fn(&'r str) -> String {capitalize_first}>`, but its trait bounds were not satisfied
  --> exercises/standard_library_types/iterators2.rs:24:45
   |
24 |       words.into_iter().map(capitalize_first).collect()
   |                                               ^^^^^^^ method cannot be called on `Map<std::slice::Iter<'_, &str>, for<'r> fn(&'r str) -> String {capitalize_first}>` due to unsatisfied trait bounds
   |
   = note: the following trait bounds were not satisfied:
           `<for<'r> fn(&'r str) -> String {capitalize_first} as FnOnce<(&&str,)>>::Output = _`
           which is required by `Map<std::slice::Iter<'_, &str>, for<'r> fn(&'r str) -> String {capitalize_first}>: Iterator`
           `for<'r> fn(&'r str) -> String {capitalize_first}: FnMut<(&&str,)>`
           which is required by `Map<std::slice::Iter<'_, &str>, for<'r> fn(&'r str) -> String {capitalize_first}>: Iterator`
           `Map<std::slice::Iter<'_, &str>, for<'r> fn(&'r str) -> String {capitalize_first}>: Iterator`
           which is required by `&mut Map<std::slice::Iter<'_, &str>, for<'r> fn(&'r str) -> String {capitalize_first}>: Iterator`

error: aborting due to 2 previous errors

Some errors have detailed explanations: E0599, E0631.
For more information about an error, try `rustc --explain E0599`.

However, it works fine after I changed .map(capitalize_first) to .map(|x| capitalize_first(x)). Apparently, Rust borrows (not sure mutable or immutable) each item before passing it to the argument closure/function of map, which makes sense because we typically don't want to consume the objects being iterated over.

What I cannot understand is why Rust doesn't borrow the arguments to |x| capitalize_first(x). I hypothesize that the closure |x| capitalize_first(x) still got &&str, and then the auto-dereferencing rules kicked in and dereferenced it to &str, but that doesn't explain why it didn't kick in when I'm using the function capitalize_first. What's the difference between .map(|x| capitalize_first(x)) and .map(capitalize_first)? Is dynamic dispatch happening here, given that the argument to map is a trait object?

Note: this question is not a duplicate to Using a function with iter().map() - as a named function vs as a closure because I am asking why, whereas the other post asked how. Regarding the accepted answer to that question, I would appreciate it if someone could explain why we need AsRef while Rust already has auto-dereferencing rules.


Solution

  • Why can I call capitalize_first with a &&str argument?

    The linked Q&A for auto-dereferencing rules is specifically for how self is resolved when using the a.b() syntax. The rules for arguments in general skip the auto-reference step and just rely on deref coercions. Since &&str implements Deref<Target = &str> (and indeed all references implement Deref), this &&str -> &str transformation happens transparently.

    Why doesn't it work for .map(f) then?

    Plain and simply, map() is expecting something that implements Fn(&&str) -> T and capitalize_first does not. A Fn(&str) is not transparently transformed into a Fn(&&str), it requires a transformation step like the one introduced by the closure (albeit transparently).