Understanding Rust functional programming styles

I'm a very beginner to Rust and I got confused by Rust functional programming style code. Assume freq is a vector containing non negative integers. The below code makes me confused.

Get the largest index with a positive value.

let key_max = (0..denom).rev().step_by(1).filter(|&i| freq[i] > 0).next().unwrap();

Get the number of positive values in freq.

let size = freq.iter().filter(|&&f| f > 0u32).count();

I wonder

  1. why the first line of code use &i (a single &) while the second line of code uses &&f (double &).
  2. In general, why do we need &&f in the second line of code? Why doesn't Rust make it simple to require only a single &?

It's because filter always adds a reference to the item type, as it doesn't let you take ownership of the value.

In the first case, (0..denom) creates an iterator of item type usize. The argument to filter will then be &usize.

In the second case, freq.iter() creates an iterator of item type &u32, so the argument to filter will then be &&u32.

6 Likes

Yeah, this is a bit unfortunate.

Don't feel bad if you forget to put the &s, though, or put the wrong number of them. I certainly still get it wrong regularly. Getting compiler errors isn't a bad thing; it's totally ok to just write it without any then put them in where the compiler says they're needed :upside_down_face:

1 Like

Hi Alice,

Thank you very much for your explanation! Would you help explain a little bit more why 0..denom creates an iterator of item type usize (without reference) while freq.iter() creates an iterator of item type &u32? Is it because freq is mutable? If I make freq non mutable, will freq.iter() creates an iterator of item type u32?

0..denom is sugar for creating a Range (you can search for .. in the docs to find that out) and you can look in the docs to see the type of items that will come out of iterating it Range in std::ops - Rust
image

You can similarly look up .iter() on a vec to see its iterator type:

But why is it this way? Because of whether the items need to be available later. One can iterate a Vec with .iter() multiple times -- even simultaneously. So it gives out borrows, rather than the items themselves. If you want to consume the Vec as part of iterating -- meaning one couldn't get the values out of it again afterwards -- then you can use .into_iter() and it'll give you an owned T instead of the &T that .iter() gives you. (Of course, for something trivial like usizes, you can also do things like .iter().copied() to get an iterator that gives you usizes instead of &usizes without consuming the vector.)

3 Likes

Hi scotmcm,

Thank you very much for the explanation and information.

I had a try on an immutable vector myself. Both a single and double & works. Why is that? Does && has special meanings in this case? It somehow reminds of the universal reference in C++ (hasn't use C++ for many years).

It is because the Rem trait is implemented for both the combinations &i32 % i32 and i32 % i32, so you can compute modulo with both an integer and a reference to an integer.

Indeed in the first case, x has type &i32 and in the latter, x has type i32.

3 Likes

Thank you both very much on the good explanations. Now things make more sense to me!

Talking about modulus, I found Rust to be really peaky (comes with safety) about types. For example, Rust complains about apply % to different types of integers. To me, this is natural and should be supported and return the smaller type.

Do you think is a feature that I can submit a request to the dev team?

For the last conversation about this I remember, see RFC: implement Rem for all equally signed integers where RHS < LHS by lcnr · Pull Request #2643 · rust-lang/rfcs · GitHub

Won't go anywhere. Rust is deliberately picky about numeric types, because often, even when it feels natural, it hides footguns in the edge cases. If you support %, do you also support /? If so, what's 1000u64 / 2u8, and is it the same as 1000u64 % 2u8? These issues have been hashed over repeatedly, and the consensus is that it's better to encourage developers to be explicit about their numeric types, and to consider them carefully as part of application design.

3 Likes

% is different in that it is guaranteed that the resulting value is storable in the RHS type. That's not the case for any other arithmetic operation among integers.

I think it would be wiser to wait with submitting type-system-related proposals until you are well-acquainted with the type system.

Fortunately, no, not at all. Rust has nothing weird like the different kinds of C++ references, and && is not special. Rust "references" are much more like pointers, but with added type-level safety features, and thus, && is more like a pointer-to-pointer-to-T.

Again, it manifests in the argument of filter() because filter always passes a reference to the current item to the predicate closure, since it can't pass it by value. If the predicate yields true, then filter has to return the value of the current item, which wouldn't be possible if it transferred its ownership into the predicate.

And this is also one of the reasons that filter_map exists -- that way you get ownership of the thing in the closure, but you have to pass it back out of the closure again if you want to get it out of the iterator adapter.

1 Like

Thank everyone for the good discussion here! I've learned more than I originally expected. Rust community is indeed among the most friendly ones.

While things become clearer to me, Rust does sounds more interesting. Looking forward to lean more about Rust and use it for real projects.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.