Flat_map - the type of this value must be known in this context


#1

I created a small crate that validates IBAN’s. It is (very originally) called iban_validate. For computing the checksum of an address, I use the following code:

changed_order.chars()
    // Convert letters to numbers
    .flat_map(|c| c.to_digit(36).unwrap().to_string().chars())
    // Convert chars to digits
    .map(|c| c.to_digit(10).unwrap())
    // Compute modulo
    .fold(0, |acc, d| (acc * 10 + d) % 97) as u8

When I run it, I get the following error:

error: borrowed value does not live long enough
  --> src/lib.rs:70:23
   |
70 |         .flat_map(|c| c.to_digit(36).unwrap().to_string().chars())
   |                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^       - temporary value only lives until here
   |                       |
   |                       temporary value created here
...
74 |         .fold(0, |acc, d| (acc * 10 + d) % 97) as u8
   |                                              - temporary value needs to live until here

What am I doing wrong and how can I fix it? Here is a playground where the error occurs: https://is.gd/5TR0gP.


#2

to_string() allocates a new string. Rust needs to know when and how to free it.
So it needs to be stored somewhere. Currently you have it as a temporary object in a closure.

You can collect all strings to a vec first, so that they’re not “lost”.


#3

Like this? Wont this be less efficient?


#4

Perhaps you could append the intermediate digits to a String (without materializing intermediate strings), and then run your fold over that, something like:

let mut s = String::new();
    for c in changed_order.chars() {
        let _ = write!(&mut s, "{}", c.to_digit(36).unwrap());
    }
    s.chars()
        .map(|c| c.to_digit(10).unwrap())
        .fold(0, |acc, d| (acc * 10 + d) % 97) as u8

#5

I would not use strings at all:

 changed_order.chars()
    .map(|ch| { let d = ch.to_digit(36).unwrap();
                (d, if d < 10 { 10 } else { 100 }) })
    .fold(0, |acc, (d, exp)| (acc * exp + d) % 97);

or, at this point probably easier to read with just the fold,

 changed_order.chars()
              .fold(0, |acc, ch| {
                  let digit = ch.to_digit(36).unwrap();
                  let exp = if digit < 10 { 10 } else { 100 };
                  (acc * exp + digit) % 97
              });

#6

I like @birkenfeld’s simple solution, but I already wrote the following, so I’ll show it as another tool for your general toolbox.

Since you know to_digit(36) is always a 1- or 2-digit number, you can just an ArrayVec and have no allocations at all. Something like:

    .flat_map(|c| {
        let n = c.to_digit(36).unwrap() as u8;
        if n >= 10 {
            ArrayVec::from([n / 10, n % 10])
        } else {
            // hopefully this infers the same `ArrayVec<[u8;2]>`,
            // but it might need explicit type annotations...
            std::iter::once(n).collect()
        }
    }

#7

Very nice solutions. Thank you all!


#8

In the end, thanks to your help, I could reduce the entire function down to this:

/// This function computes the checksum of an address. The function assumes the string only
/// contains 0-9 and A-Z.
///
/// # Panics
/// If the address contains any characters other than 0-9 or A-Z, this function will panic.
fn compute_checksum(address: &String) -> u8 {
    address.chars()
        // Move the first four characters to the back
        .cycle()
        .skip(4)
        .take(address.len())
        // Calculate the checksum
        .fold(0, |acc, c| {
            // Convert '0'-'Z' to 0-35
            let digit = c.to_digit(36).unwrap();
            // If the number consists of two digits, multiply by 100
            let multiplier = if digit > 9 { 100 } else { 10 };
            // Calculate modulo
            (acc * multiplier + digit) % 97
        }) as u8
}