5

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.

kmdreko
  • 42,554
  • 6
  • 57
  • 106
nalzok
  • 14,965
  • 21
  • 72
  • 139
  • Related Q&A: [What is the difference between `filter(func)` and `filter(|x| func(x))`?](/q/73823725/2189130) but the problem there was a difference between *late-bound* and *early-bound* lifetimes. – kmdreko Sep 24 '22 at 16:31

2 Answers2

7

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).

kmdreko
  • 42,554
  • 6
  • 57
  • 106
  • *(and indeed all references implement Deref)* I wish you could expand on that: does `&T` always implement `Deref`, so that I can always pass a `&T` to `f(x: T)`? The document doesn't seem to mention this, so I'd better ask for some clarification. – nalzok Jun 07 '21 at 06:22
  • @nalzok no, deref coercion only work on `&T` to `&U` transformations; both have to be references. Its designed for cases like `&Vec` to `&[T]` or `&String` to `&str` or `&Box` to `&T`. – kmdreko Jun 07 '21 at 06:29
  • I see, but how does `&&str` -> `&str` fit into that picture? In general, so we have `&…&&T` -> `&…&T`? – nalzok Jun 07 '21 at 10:00
1

@kmdreko is correct that the issue is with deref coercion. Keep in mind that &[T] implements IntoIterator with Self::Item being &T. Thus, if x : &[T], then x.into_iter() will implement the Iterator with Self::Item being &T.

In your case, words.into_iter() is an Iterator with Self::Item being &&str.

The map method expects a function with domain Self::Item. words.into_iter().map(f) expects f to be a function with input of type &&str.

This creates a problem because capitalize_first takes as its input a &str, not a &&str. Thus, words.into_iter().map(capitalize_first) fails to type check.

However, when we look at words.into_iter().map(|x| capitalize_first(x)), something magical happens. The closure |x| capitalize_first(x) gets magically coerced to |x| capitalize_first(*x). This then type-checks.

Mark Saving
  • 1,752
  • 7
  • 11
  • *Keep in mind that `&[T]` implements `IntoIterator` with `Self::Item` being `&T`.* Could you point me to where it's documented? – nalzok Jun 07 '21 at 06:26
  • @nalzok https://doc.rust-lang.org/std/primitive.slice.html#impl-IntoIterator Search for "impl<'a, T> IntoIterator for &'a [T]" – Mark Saving Jun 07 '21 at 06:29