7

As a Rust newbie, I'm working through the Project Euler problems to help me get a feel for the language. Problem 4 deals with palindromes, and I found two solutions for creating a vector of palindromes, but I'm not sure how either of them work.

I'm using a vector of strings, products, that's calculated like this:

let mut products = Vec::new();
for i in 100..500 {
    for j in 500..1000 {
        products.push((i * j).to_string());
    }
}

For filtering these products to only those that are palindromic, I have the following two solutions:

Solution 1:

let palindromes: Vec<_> = products
    .iter()
    .filter(|&x| x == &x.chars().rev().collect::<String>())
    .collect();

Solution 2:

let palindromes: Vec<_> = products
    .iter()
    .filter(|&x| *x == *x.chars().rev().collect::<String>())
    .collect();

They both yield the correct result, but I have no idea why!

In Solution 1, we're comparing a reference of a string to a reference of a string we've just created?

In Solution 2, we dereference a reference to a string and compare it to a dereferenced new string?

What I would expect to be able to do:

let palindromes: Vec<_> = products
    .iter()
    .filter(|x| x == x.chars().rev().collect::<String>())
    .collect();

I'm hoping somebody will be able to explain to me:

  • What is the difference is between my two solutions, and why do they both work?
  • Why can't I just use x without referencing or dereferencing it in my filter function?

Thank you!

Boiethios
  • 38,438
  • 19
  • 134
  • 183
piercebot
  • 1,767
  • 18
  • 16
  • 1
    "They both yield the correct result, but I have no idea why!" => "Theory is when you know everything but nothing works **practice is when everything works but no one knows why** in our lab theory and practice are combined nothing works and no one knows why" – Stargateur May 02 '18 at 11:42
  • 1
    I think this is very similar to [How to iterate over an array?](https://stackoverflow.com/q/30467085/3650362) (poor title notwithstanding). Do the answers to that question clear things up at all? – trent May 02 '18 at 12:11
  • 4
    Note that you do not need to allocate a `String`, you can compare the iterators: `.filter(|x| x.chars().eq(x.chars().rev()))`. – Boiethios May 02 '18 at 12:44
  • 3
    @piercebot Nice, Project Euler is also how I started rust. May I challenge you to come up with a palindrome check that does not require allocation (`collect` creates a new `String` on the heap)? :) – MB-F May 02 '18 at 12:44
  • Well.. so much for challenge :p – MB-F May 02 '18 at 12:51
  • @Boiethios I didn't know about `.eq`, this is super duper! Thanks! Doing it the brute-force way was a good way to run into pitfalls though; thanks @kazemakase for the lesson :) – piercebot May 02 '18 at 14:48

1 Answers1

11
  1. Vec<String>.iter() returns an iterator over references (&String).
  2. The closure argument of .filter() takes a reference to an iterator's item. So the type that is passed to the closure is a double reference &&String.
  3. |&x| tells the closure to expect a reference, so x is now of type &String.

First solution: collect returns a String, of which & takes the reference. x is also a reference to a string, so the comparison is between two &String.

Second solution: The dereference operator * is applied to x, which results in a String. The right hand side is interesting: The String result of collect is dereferenced. This results in a string slice because String implements Deref<Target=str>. Now the comparison is between String and str, which is works because it is implemented in the standard library (Note that a == b is equivalent to a.eq(&b)).

Third solution: The compiler explains why it does not work.

the trait std::cmp::PartialEq<std::string::String> is not implemented for &&std::string::String

The left side is a double reference to string (&&String) and the right side is just a String . You need to get both sides to the same "reference level". All of these work:

x.iter().filter(|x| x == &&x.chars().rev().collect::<String>());
x.iter().filter(|x| *x == &x.chars().rev().collect::<String>());
x.iter().filter(|x| **x == x.chars().rev().collect::<String>());
Stargateur
  • 24,473
  • 8
  • 65
  • 91
MB-F
  • 22,770
  • 4
  • 61
  • 116
  • Fantastic! Thank you for the thorough explanation! Are all closure arguments references by default, or is it a case-by-case basis? – piercebot May 02 '18 at 14:44
  • 4
    @piercebot Case by case. If you look at the definition of [`filter`](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.filter) you can see that it requires a closure that takes a `&Iter::Item`. In contrast, for example the closure accepted by `filter_map` takes `Iter::Item`, which is not a reference. – MB-F May 02 '18 at 14:56