rust-lang / rustlings

:crab: Small exercises to get you used to reading and writing Rust code!
https://rustlings.cool
MIT License
54.01k stars 10.15k forks source link

I don't understand why `iterators2` works #2129

Closed egolep closed 3 weeks ago

egolep commented 3 weeks ago

I was doing the iterators2 exercise and I had completed capitalize_words_vector as:

fn capitalize_words_vector(words: &[&str]) -> Vec<String> {
    words.iter().map(|word| capitalize_first(word)).collect()
}

which is the right solution. Then, I write capitalize_words_string as:

fn capitalize_words_string(words: &[&str]) -> String {
    words.iter().map(|word| capitalize_first(word)).reduce(|a, b| a + &b).unwrap()
}

which also works fine. Out of curiosity, I looked at the solution and find out that capitalize_word_string is written as:

fn capitalize_words_string(words: &[&str]) -> String {
    words.iter().map(|word| capitalize_first(word)).collect()

which is exactly the same as capitalize_word_vector but with a different return type. How can this be correct? Does the compiler looks at the return type and automatically add a .reduce?

frroossst commented 3 weeks ago

I am by no means a rust expert, but here is my best attempt at an explanation.

If you look at the implementation of the .collect() in iterator ref, you'll notice a <B> in the function definition, which means it is a generic. You can learn more about generics here.

Basically, the collect() method is implemented for Vec<String> and String and for many other types. So, when yo do the capitalize_words_vector the compiler infers that the variant of the collect method to be used must be the one that returns Vec<String> and in the function capitalize_words_string would use the variant or version of the collect method that returns the type String

Hint: you'll notice that the reduce method is also generic. [ref](fn reduce(self, f: F) -> Option<Self::Item>)

Hopefully that answers your question, feel free to ask more!

egolep commented 3 weeks ago

Thanks @frroossst for your reply. It makes it definitely more clear (I also read the .collect() documentation in the meanwhile) and I now generally understand how it work. I still have a doubt, though: how can I understand how the .collect() method will work before using it? Is there a general criterion I can refer to or do I have to go and look for the specific implementation of the method for the type I'm going to use?

frroossst commented 3 weeks ago

Also, here is a simple example to further illustrate what generics do

fn larger<T: PartialOrd>(a: T, b: T) -> T {
    if a > b {
        a
    } else {
        b
    }
}

fn main() {
    let x = 5;
    let y = 10;
    let result = larger::<i32>(x, y);
    // let result = larger::<i32>(x, y);
    println!("The larger value is: {}", result);

    let s1 = "apple";
    let s2 = "banana";
    let result_str = larger::<&str>(s1, s2);
    // let result_str = larger::<&str>(s1, s2);
    println!("The larger string is: {}", result_str);
}

the larger function is generic, meaning it can work for multiple data types, in this example it works for both &str and i32, now we can help the compiler explicitly using the <type> syntax or we can leave that out and let the compiler automatically figure out what version or variant of the generic function to use. If you were using larger on your own structs or enum type for example, you would have to implement the PartialOrd trait.

But to answer your question. I think a lot of knowing which method to use comes from writing code. Suppose you have never had to use the * operator to multiply two numbers over. So, you wouldn't need to know that it even exists. But one day, you need to multiply two numbers so you look at the documentation and find out that the * operator exists. Now, the next time you need to solve a similar problem, you'll be like hmm... I've run into this before, what operator was it? and you look again, and go AH! it was the * operator. And by the third and fourth time, you'll simply remember.

The more you use something the more likely you are to remember how to use it and what to use to solve your problem.

apologies for the long answer, hope it helped.

egolep commented 3 weeks ago

That's super useful, thanks you. Especially the possibility to suggest the specific type to use for the generics. However, my doubt is slightly different: let's say I decided to use .collect() for String. Before using it, how can I know that the result will be the concatenation of all the strings instead of, for example, a different merge where I take the first letter from all strings, then the second and so on? I guess that the simpler answer is always the right one. I'm afraid there may be corner cases where it is not easy to understand what the simpler answer is, but this is probably what you mean with "I think a lot of knowing which method to use comes from writing code". Thank you again for your replies, they were very useful and insightful.