Having trouble sorting out type annotations and lifetime annotations in toy code


#1

Here’s a piece of code, meant to deal with iterators over characters of a string:

struct PairedIterator<'a, A: 'a> where
    A: Iterator<Item = char> {
        iterator: A,
        jumped_zip: std::iter::Zip<&'a mut A, &'a mut A>,
        size: usize,
        counter: usize,
    }

impl<'a, A: Iterator<Item = char>> PairedIterator<'a, A> {
    fn new(num_str: &str, jump: usize) -> PairedIterator<A> {
        let mut chars_iterator = num_str.chars().cycle();
        let mut jumped_iterator = chars_iterator.by_ref();
        jumped_iterator.nth(jump);

        PairedIterator { 
            iterator: chars_iterator, 
            jumped_zip: chars_iterator.by_ref().zip(jumped_iterator),
            counter: 0,
            size: num_str.len(),
        }
    }

    fn consume_and_parse(&mut self) -> (u32, u32) {
        let (a, b) = self.jumped_zip.next().expect("could not get next pair!");
        self.counter = self.counter + 1;

        (a.to_digit(10).expect("could not parse char as digit!"), 
        b.to_digit(10).expect("could not parse char as digit!"))
    }
}

fn main() {
    let sum = 0;

    println!("sum: {}", sum);
}

There are two issues with this:

  1. the compiler is going to complain about wanting a type parameter, but finding something else; I believe that issue is related to the SO answer found here: https://stackoverflow.com/a/32551408/3486684

My problem is that I am not sure how to further specialize the impl, should I use something like: impl PairedIterator<std::str::Chars>? But then, should I not be using the generic type A in the first place and replace it everywhere by std::str::Chars, that way, I don’t even have to bother specializing impl?

  1. Regardless of how issue 1 is fixed, the compiler will complain about lifetime annotations. I think the issue is that when creating a new PairedIterator, I take a string and get an iterator over its chars, but I guess if the string is deallocated, then the iterator might have issues, because it was dependent on that string? To be honest, I am not sure I totally understand the lifetime issues here, but I can “sense” them…can you help clarify what I would need to do to make the compiler and myself happy?

#2

#1 is exactly that SO answer.

#2 is that in Rust one field of struct cannot borrow from a different field of the same struct. It must borrow from something that lives longer.

I think this is going to be a matter of writing out the correct type for jumped_zip based on how it is being initialized. Something like this could work:

use std::iter::{Cycle, Skip, Zip};
use std::str::Chars;

pub struct PairedIterator<'a> {
    jumped_zip: Zip<Cycle<Chars<'a>>, Skip<Cycle<Chars<'a>>>>,
}

impl<'a> PairedIterator<'a> {
    pub fn new(num_str: &'a str, jump: usize) -> Self {
        let chars_iterator = num_str.chars().cycle();
        let jumped_iterator = chars_iterator.clone().skip(jump);
        PairedIterator {
            jumped_zip: chars_iterator.zip(jumped_iterator),
        }
    }

    pub fn consume_and_parse(&mut self) -> (u32, u32) {
        let (a, b) = self.jumped_zip.next().expect("could not get next pair!");
        (a.to_digit(10).expect("could not parse char as digit!"), 
        b.to_digit(10).expect("could not parse char as digit!"))
    }
}

#3

What if I put down the hard restriction that there can only be one iterator copy (no cloning), so jumped_zip has to be formed by zipping together iterators that are by reference? How would one then write that? I know there is no particular need to “save memory”, so I am just asking really for the sake of an exercise. Assume that num_str was HUGE, so that making a clone of it would be problematic?


#4

The code does not clone num_str, it clones a Cycle<Chars<'a>> that borrows from num_str. A total of 4 pointers are copied regardless of how big num_str is.


#5

Oh! I think my question is: why did you not have to use the by_ref method on Cycle<Chars<'a>> to get a version of it by reference? When I look at the documentation for Cycle, it does not suggest that it returns an iterator by reference? Sorry, I think I am misunderstanding some simple stuff!

Edit: I think I understand now. The iterator Chars is produced by calling the char method of a string slice. Recall that a string slice is a reference (or perhaps more accurately: a collection of contiguous references?), so when we call the chars method, we get back an iterator over that collection of references. Copying/cloning that iterator does not copy the underlying string data which the string slice refers to, it merely copies the iterator over a bunch of references.